From 0b3ee491eb08912427b0df601ba9896c7e4b668c Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:48:19 -0300 Subject: [PATCH 01/15] docs: add Torii client integration overview guide - Create main overview page with quick start guide - Include basic setup instructions and working examples - Add links to detailed guides for comprehensive learning - Provide copy-paste ready code for immediate use - Address GitHub issue #194 for Torii client integration documentation --- .../deployment/torii/client-integration.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 client-new/pages/deployment/torii/client-integration.md diff --git a/client-new/pages/deployment/torii/client-integration.md b/client-new/pages/deployment/torii/client-integration.md new file mode 100644 index 0000000..d328147 --- /dev/null +++ b/client-new/pages/deployment/torii/client-integration.md @@ -0,0 +1,168 @@ +# Torii Client Integration Guide + +Learn how to consume Torii data from your React application using GraphQL queries and custom hooks. This guide provides practical examples for fetching game state, player data, and other onchain information efficiently from Dojo's Entity Component System (ECS) architecture. + +## 🎯 Quick Start + +### Prerequisites +- Torii indexer running with your deployed Dojo world +- React application with `@starknet-react/core` installed +- Deployed Cairo models and systems (e.g., Position, Moves models) + +### Basic Setup + +1. **Install dependencies**: +```bash +npm install @starknet-react/core +``` + +2. **Configure environment variables**: +```bash +# .env.local +VITE_TORII_URL=http://localhost:8080 +VITE_WORLD_ADDRESS=0x1234567890abcdef... +``` + +3. **Update dojoConfig**: +```typescript +// src/dojo/dojoConfig.ts +export const dojoConfig = createDojoConfig({ + manifest, + toriiUrl: VITE_PUBLIC_TORII || '', + // ... other config +}); +``` + +4. **Create a basic hook**: +```typescript +// src/hooks/usePlayer.ts +import { useEffect, useState } from "react"; +import { useAccount } from "@starknet-react/core"; + +export const usePlayer = () => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const { account } = useAccount(); + + // Fetch data from Torii + const fetchData = async () => { + if (!account?.address) return; + + const response = await fetch(`${dojoConfig.toriiUrl}/graphql`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: ` + query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + } + `, + variables: { playerOwner: account.address } + }) + }); + + const result = await response.json(); + setPosition(result.data?.fullStarterReactPositionModels?.edges[0]?.node); + setIsLoading(false); + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + return { position, isLoading, refetch: fetchData }; +}; +``` + +## 📚 Detailed Guides + +For comprehensive implementation details, explore these focused guides: + +### 🔧 **Setup & Configuration** +- **[Basic Setup](./guides/setup.md)** - Environment configuration and dependencies +- **[GraphQL Queries](./guides/graphql-queries.md)** - Query patterns and model structure + +### 🎣 **Implementation Patterns** +- **[Custom Hooks](./guides/custom-hooks.md)** - Hook patterns and state management +- **[Data Conversion](./guides/data-conversion.md)** - Hex to number utilities and type handling +- **[Error Handling](./guides/error-handling.md)** - Error management and retry strategies + +### ⚡ **Advanced Features** +- **[Performance Optimization](./guides/performance.md)** - Caching and optimization strategies +- **[Security Best Practices](./guides/security.md)** - Address validation and sanitization +- **[Testing Strategies](./guides/testing.md)** - Hook testing and validation + +### 🚀 **Production Deployment** +- **[Production Patterns](./guides/production.md)** - Environment-specific configurations +- **[Common Patterns](./guides/common-patterns.md)** - Multiple model queries and leaderboards + +## 🎮 Quick Examples + +### Basic Component Usage +```typescript +import { usePlayer } from '../hooks/usePlayer'; + +export const PlayerInfo = () => { + const { position, isLoading, refetch } = usePlayer(); + + if (isLoading) return
Loading...
; + + return ( +
+

Player Position

+ {position && ( +
+

X: {position.x}

+

Y: {position.y}

+
+ )} + +
+ ); +}; +``` + +### Data Conversion +```typescript +// Convert hex values from Cairo models +const hexToNumber = (hexValue) => { + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + return hexValue; +}; + +// Usage +const position = { + ...rawPosition, + x: hexToNumber(rawPosition.x), + y: hexToNumber(rawPosition.y) +}; +``` + +## 🔗 Additional Resources + +- **[Dojo Game Starter](https://github.com/AkatsukiLabs/Dojo-Game-Starter)** - Complete working example +- **[Torii Documentation](https://github.com/dojoengine/torii)** - Official Torii docs +- **[React Integration Overview](../guides/react/overview.md)** - React-specific patterns +- **[Dojo Configuration](../guides/react/dojo-config.md)** - Setup details + +## 🎯 Next Steps + +1. **Start with [Basic Setup](./guides/setup.md)** for configuration +2. **Learn [GraphQL Queries](./guides/graphql-queries.md)** for data fetching +3. **Implement [Custom Hooks](./guides/custom-hooks.md)** for state management +4. **Add [Error Handling](./guides/error-handling.md)** for reliability +5. **Optimize with [Performance](./guides/performance.md)** for production + +--- + +**Need help?** Check out the [Common Issues](./guides/common-patterns.md#common-issues) section or explore the detailed guides above. From 3e3616dea710d9b7261e00d4d97fdbf0a3d7cbe0 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:48:36 -0300 Subject: [PATCH 02/15] docs: add Torii setup and configuration guide - Add comprehensive setup instructions for Torii integration - Include environment configuration for different networks - Provide Dojo configuration examples with security best practices - Add troubleshooting section for common setup issues - Include address validation and security considerations --- .../pages/deployment/torii/guides/setup.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/setup.md diff --git a/client-new/pages/deployment/torii/guides/setup.md b/client-new/pages/deployment/torii/guides/setup.md new file mode 100644 index 0000000..36d33c6 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/setup.md @@ -0,0 +1,202 @@ +# Torii Setup & Configuration + +Learn how to set up your React application to work with Torii's GraphQL interface for querying Dojo's on-chain game state. + +## 📋 Prerequisites + +Before you begin, ensure you have: + +- **Torii indexer running** with your deployed Dojo world +- **React application** with `@starknet-react/core` installed +- **Deployed Cairo models and systems** (e.g., Position, Moves models) +- **Environment variables** configured for your network +- **Dojo world address** and contract manifest + +## 📦 Required Dependencies + +Install the necessary packages: + +```bash +npm install @starknet-react/core +# or +pnpm add @starknet-react/core +``` + +## ⚙️ Environment Configuration + +### Environment Variables + +Create a `.env.local` file in your project root: + +```bash +# Local Development +VITE_TORII_URL=http://localhost:8080 +VITE_WORLD_ADDRESS=0x1234567890abcdef... + +# Testnet (Sepolia) +VITE_TORII_URL=https://api.cartridge.gg/x/your-game/torii/graphql +VITE_WORLD_ADDRESS=0xabcdef1234567890... + +# Production (Mainnet) +VITE_TORII_URL=https://api.cartridge.gg/x/your-game/torii/graphql +VITE_WORLD_ADDRESS=0x1234567890abcdef... +``` + +### Network-Specific Configuration + +#### Local Development +```bash +VITE_PUBLIC_NODE_URL=http://localhost:5050 +VITE_PUBLIC_TORII=http://localhost:8080 +VITE_PUBLIC_MASTER_ADDRESS=0x123...abc +VITE_PUBLIC_MASTER_PRIVATE_KEY=0x456...def +``` + +#### Testnet (Sepolia) +```bash +VITE_PUBLIC_NODE_URL=https://api.cartridge.gg/x/starknet/sepolia +VITE_PUBLIC_TORII=https://api.cartridge.gg/x/your-game/torii/graphql +VITE_PUBLIC_MASTER_ADDRESS=0x123...abc +VITE_PUBLIC_MASTER_PRIVATE_KEY=0x456...def +``` + +#### Production (Mainnet) +```bash +VITE_PUBLIC_NODE_URL=https://api.cartridge.gg/x/starknet/mainnet +VITE_PUBLIC_TORII=https://api.cartridge.gg/x/your-game/torii/graphql +VITE_PUBLIC_MASTER_ADDRESS= +VITE_PUBLIC_MASTER_PRIVATE_KEY= +``` + +## 🔧 Dojo Configuration + +### Basic Configuration + +Update your `dojoConfig.js` to include Torii URL: + +```typescript +// src/dojo/dojoConfig.ts +import { createDojoConfig } from "@dojoengine/core"; +import { manifest } from "../config/manifest"; + +const { + VITE_PUBLIC_NODE_URL, + VITE_PUBLIC_TORII, + VITE_PUBLIC_MASTER_ADDRESS, + VITE_PUBLIC_MASTER_PRIVATE_KEY, +} = import.meta.env; + +export const dojoConfig = createDojoConfig({ + manifest, + masterAddress: VITE_PUBLIC_MASTER_ADDRESS || '', + masterPrivateKey: VITE_PUBLIC_MASTER_PRIVATE_KEY || '', + rpcUrl: VITE_PUBLIC_NODE_URL || '', + toriiUrl: VITE_PUBLIC_TORII || '', +}); +``` + +### Production-Safe Configuration + +For production deployments, ensure master credentials are not included: + +```typescript +// ✅ Production-safe configuration +export const dojoConfig = createDojoConfig({ + manifest, + masterAddress: '', // Never include in production + masterPrivateKey: '', // Never include in production + rpcUrl: VITE_PUBLIC_NODE_URL || 'https://api.cartridge.gg/x/starknet/mainnet', + toriiUrl: VITE_PUBLIC_TORII || 'https://api.cartridge.gg/x/your-game/torii/graphql', +}); +``` + +## 🎯 Common Setup Issues + +### Issue: "Can't connect to Torii" + +**Solution**: Check your Torii URL and ensure the indexer is running. + +```typescript +// Test Torii connection +const testConnection = async () => { + try { + const response = await fetch(`${dojoConfig.toriiUrl}/graphql`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: "{ __schema { types { name } } }" + }) + }); + return response.ok; + } catch { + return false; + } +}; +``` + +### Issue: "GraphQL queries return empty results" + +**Solution**: Verify your Cairo models are deployed and indexed. + +```typescript +// Debug query to check if models exist +const DEBUG_QUERY = ` + query DebugModels { + fullStarterReactPositionModels(first: 1) { + edges { + node { + player + x + y + } + } + } + } +`; +``` + +### Issue: "Environment variables not loading" + +**Solution**: Ensure your environment file is in the correct location and format. + +```bash +# Check if variables are loaded +console.log('Torii URL:', import.meta.env.VITE_PUBLIC_TORII); +console.log('World Address:', import.meta.env.VITE_WORLD_ADDRESS); +``` + +## 🔒 Security Considerations + +### Environment Variable Security + +- **Never commit** `.env.local` files to version control +- **Use different values** for development, staging, and production +- **Validate URLs** before making requests +- **Sanitize addresses** before using in queries + +### Address Validation + +```typescript +// Basic Starknet address validation +const validateContractAddress = (address: string): boolean => { + const addressRegex = /^0x[a-fA-F0-9]{63}$/; + return addressRegex.test(address); +}; + +// Usage +if (!validateContractAddress(account.address)) { + throw new Error('Invalid contract address'); +} +``` + +## 📚 Next Steps + +Once your setup is complete: + +1. **Learn GraphQL queries** in the [GraphQL Queries](./graphql-queries.md) guide +2. **Create custom hooks** using the [Custom Hooks](./custom-hooks.md) guide +3. **Handle data conversion** with the [Data Conversion](./data-conversion.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [GraphQL Queries](./graphql-queries.md) From dd36e62fcec8e0239e453fafca1460d775b3c499 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:48:55 -0300 Subject: [PATCH 03/15] docs: add GraphQL queries guide for Torii integration - Add comprehensive GraphQL query structure documentation - Include Cairo model query examples (Position, Moves) - Provide advanced query patterns (pagination, filtering, sorting) - Add query optimization best practices - Include debugging and schema introspection examples --- .../torii/guides/graphql-queries.md | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/graphql-queries.md diff --git a/client-new/pages/deployment/torii/guides/graphql-queries.md b/client-new/pages/deployment/torii/guides/graphql-queries.md new file mode 100644 index 0000000..022cfaf --- /dev/null +++ b/client-new/pages/deployment/torii/guides/graphql-queries.md @@ -0,0 +1,389 @@ +# GraphQL Queries for Torii + +Learn how to structure GraphQL queries to fetch Cairo model data from Torii's indexing service. + +## 🔍 Query Structure Overview + +Torii generates GraphQL queries based on your Cairo models. The naming convention follows: `full{WorldName}{ModelName}Models`. + +### Basic Query Format + +```graphql +query QueryName($variable: Type!) { + fullWorldNameModelNameModels(where: { field: $variable }) { + edges { + node { + field1 + field2 + } + } + } +} +``` + +## 📊 Cairo Model Query Examples + +### Position Model Query + +```graphql +# Query for Position model (Cairo struct with #[key] player field) +query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } +} +``` + +### Moves Model Query + +```graphql +# Query for Moves model (Cairo struct with #[key] player field) +query GetPlayerMoves($playerOwner: ContractAddress!) { + fullStarterReactMovesModels(where: { player: $playerOwner }) { + edges { + node { + player + remaining + } + } + } +} +``` + +### Multiple Model Query + +```graphql +# Query multiple Cairo models for a player +query GetPlayerState($playerOwner: ContractAddress!) { + position: fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { + edges { + node { + player + remaining + } + } + } +} +``` + +## 🎯 Advanced Query Patterns + +### Leaderboard Query + +```graphql +# Query top players by remaining moves +query GetTopPlayers { + fullStarterReactMovesModels( + first: 10, + orderBy: "remaining", + orderDirection: "desc" + ) { + edges { + node { + player + remaining + } + } + } +} +``` + +### Filtered Query + +```graphql +# Query players with specific conditions +query GetActivePlayers { + fullStarterReactMovesModels( + where: { remaining: { gt: "0" } }, + first: 50, + orderBy: "remaining", + orderDirection: "desc" + ) { + edges { + node { + player + remaining + } + } + } +} +``` + +### Pagination Query + +```graphql +# Query with pagination +query GetPlayersPage($first: Int!, $after: String) { + fullStarterReactMovesModels( + first: $first, + after: $after, + orderBy: "remaining", + orderDirection: "desc" + ) { + edges { + node { + player + remaining + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} +``` + +## 🔧 Query Variables + +### Variable Types + +```typescript +// Common variable types for Torii queries +interface QueryVariables { + playerOwner: string; // ContractAddress + first: number; // Int + after: string; // String (cursor) + orderBy: string; // String (field name) + orderDirection: 'asc' | 'desc'; // String +} +``` + +### Using Variables + +```typescript +// Example query with variables +const query = ` + query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + } +`; + +const variables = { + playerOwner: account.address +}; + +const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }) +}); +``` + +## 📋 Query Best Practices + +### 1. Use Specific Field Selection + +```graphql +# ✅ Good: Select only needed fields +query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } +} + +# ❌ Avoid: Selecting all fields +query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + # Don't select fields you don't need + } + } + } +} +``` + +### 2. Use Pagination for Large Datasets + +```graphql +# ✅ Good: Use pagination +query GetPlayers($first: Int!, $after: String) { + fullStarterReactMovesModels( + first: $first, + after: $after + ) { + edges { + node { + player + remaining + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### 3. Use Filters to Reduce Data + +```graphql +# ✅ Good: Use filters to get relevant data +query GetActivePlayers { + fullStarterReactMovesModels( + where: { remaining: { gt: "0" } }, + first: 20 + ) { + edges { + node { + player + remaining + } + } + } +} +``` + +## 🛠️ Debugging Queries + +### Schema Introspection + +```graphql +# Get available models and fields +query IntrospectSchema { + __schema { + types { + name + fields { + name + type { + name + } + } + } + } +} +``` + +### Model Discovery + +```graphql +# Check if a specific model exists +query CheckModel { + fullStarterReactPositionModels(first: 1) { + edges { + node { + __typename + } + } + } +} +``` + +### Error Handling + +```typescript +// Handle GraphQL errors +const handleQueryError = (result: any) => { + if (result.errors) { + console.error('GraphQL Errors:', result.errors); + throw new Error(result.errors[0].message); + } + return result.data; +}; + +// Usage +const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }) +}); + +const result = await response.json(); +const data = handleQueryError(result); +``` + +## 🎯 Common Query Patterns + +### Single Entity Query + +```graphql +# Query a single entity by key +query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } +} +``` + +### List Query + +```graphql +# Query multiple entities +query GetAllPlayers { + fullStarterReactMovesModels(first: 100) { + edges { + node { + player + remaining + } + } + } +} +``` + +### Aggregation Query + +```graphql +# Query with aggregation (if supported) +query GetPlayerStats { + fullStarterReactMovesModels { + edges { + node { + remaining + } + } + } +} +``` + +## 📚 Next Steps + +1. **Learn custom hooks** in the [Custom Hooks](./custom-hooks.md) guide +2. **Handle data conversion** with the [Data Conversion](./data-conversion.md) guide +3. **Implement error handling** using the [Error Handling](./error-handling.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [Custom Hooks](./custom-hooks.md) From ebe74480ea6a4cfb68b5c23784eb6df0d48feb59 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:49:14 -0300 Subject: [PATCH 04/15] docs: add custom hooks guide for Torii integration - Add comprehensive React hooks patterns for Torii data fetching - Include basic, enhanced, and auto-refetch hook examples - Provide multiple model query patterns and list hooks - Add component usage examples with error handling - Include hook best practices and performance considerations --- .../deployment/torii/guides/custom-hooks.md | 510 ++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/custom-hooks.md diff --git a/client-new/pages/deployment/torii/guides/custom-hooks.md b/client-new/pages/deployment/torii/guides/custom-hooks.md new file mode 100644 index 0000000..87fe800 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/custom-hooks.md @@ -0,0 +1,510 @@ +# Custom Hooks for Torii + +Learn how to create custom React hooks for fetching and managing Cairo model data from Torii. + +## 🎣 Basic Hook Structure + +### Simple Hook Example + +```typescript +// src/hooks/usePlayer.ts +import { useEffect, useState } from "react"; +import { useAccount } from "@starknet-react/core"; +import { dojoConfig } from "../dojo/dojoConfig"; + +const TORII_URL = dojoConfig.toriiUrl + "/graphql"; + +const POSITION_QUERY = ` + query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + } +`; + +export const usePlayer = () => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: POSITION_QUERY, + variables: { playerOwner: account.address } + }) + }); + + const result = await response.json(); + const positionData = result.data?.fullStarterReactPositionModels?.edges[0]?.node; + setPosition(positionData); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + const refetch = () => { + fetchData(); + }; + + return { position, isLoading, error, refetch }; +}; +``` + +## 🔄 Multiple Model Hook + +### Hook for Multiple Models + +```typescript +// src/hooks/usePlayerState.ts +import { useEffect, useState } from "react"; +import { useAccount } from "@starknet-react/core"; + +const PLAYER_STATE_QUERY = ` + query GetPlayerState($playerOwner: ContractAddress!) { + position: fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { + edges { + node { + player + remaining + } + } + } + } +`; + +export const usePlayerState = () => { + const [playerState, setPlayerState] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchPlayerState = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: PLAYER_STATE_QUERY, + variables: { playerOwner: account.address } + }) + }); + + const result = await response.json(); + setPlayerState(result.data); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchPlayerState(); + }, [account?.address]); + + return { playerState, isLoading, error, refetch: fetchPlayerState }; +}; +``` + +## 🎯 Enhanced Hook with Data Conversion + +### Hook with Hex Conversion + +```typescript +// src/hooks/usePlayerEnhanced.ts +import { useEffect, useState } from "react"; +import { useAccount } from "@starknet-react/core"; + +// Data conversion utilities +const hexToNumber = (hexValue: string | number) => { + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + return Number(hexValue); +}; + +const formatAddress = (address: string) => { + return address.toLowerCase(); +}; + +export const usePlayerEnhanced = () => { + const [position, setPosition] = useState(null); + const [moves, setMoves] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const [positionData, movesData] = await Promise.all([ + fetchPlayerPosition(account.address), + fetchPlayerMoves(account.address) + ]); + + // Convert hex values to numbers + if (positionData) { + setPosition({ + ...positionData, + x: hexToNumber(positionData.x), + y: hexToNumber(positionData.y), + player: formatAddress(positionData.player) + }); + } + + if (movesData) { + setMoves({ + ...movesData, + remaining: hexToNumber(movesData.remaining), + player: formatAddress(movesData.player) + }); + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + const refetch = () => { + fetchData(); + }; + + return { position, moves, isLoading, error, refetch }; +}; +``` + +## 📊 List Hook Pattern + +### Hook for Lists and Leaderboards + +```typescript +// src/hooks/useLeaderboard.ts +import { useEffect, useState } from "react"; + +const LEADERBOARD_QUERY = ` + query GetTopPlayers($limit: Int!) { + fullStarterReactMovesModels( + first: $limit, + orderBy: "remaining", + orderDirection: "desc" + ) { + edges { + node { + player + remaining + } + } + } + } +`; + +export const useLeaderboard = (limit: number = 10) => { + const [players, setPlayers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchLeaderboard = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: LEADERBOARD_QUERY, + variables: { limit } + }) + }); + + const result = await response.json(); + const playersData = result.data?.fullStarterReactMovesModels?.edges || []; + + // Convert hex values + const convertedPlayers = playersData.map(({ node }: any) => ({ + ...node, + remaining: hexToNumber(node.remaining), + player: formatAddress(node.player) + })); + + setPlayers(convertedPlayers); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchLeaderboard(); + }, [limit]); + + return { players, isLoading, error, refetch: fetchLeaderboard }; +}; +``` + +## 🔄 Auto-Refetch Hook + +### Hook with Auto-Refetch on Changes + +```typescript +// src/hooks/usePlayerAutoRefetch.ts +import { useEffect, useState, useCallback } from "react"; +import { useAccount } from "@starknet-react/core"; + +export const usePlayerAutoRefetch = (refetchInterval: number = 5000) => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchData = useCallback(async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: POSITION_QUERY, + variables: { playerOwner: account.address } + }) + }); + + const result = await response.json(); + const newPosition = result.data?.fullStarterReactPositionModels?.edges[0]?.node; + + // Only update if data has changed + if (JSON.stringify(newPosition) !== JSON.stringify(position)) { + setPosition(newPosition); + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }, [account?.address, position]); + + // Initial fetch + useEffect(() => { + fetchData(); + }, [fetchData]); + + // Auto-refetch on interval + useEffect(() => { + if (!account?.address) return; + + const interval = setInterval(fetchData, refetchInterval); + return () => clearInterval(interval); + }, [fetchData, refetchInterval, account?.address]); + + return { position, isLoading, error, refetch: fetchData }; +}; +``` + +## 🎮 Component Usage Examples + +### Basic Component + +```typescript +// src/components/PlayerInfo.tsx +import React from 'react'; +import { usePlayer } from '../hooks/usePlayer'; + +export const PlayerInfo: React.FC = () => { + const { position, isLoading, error, refetch } = usePlayer(); + + if (isLoading) { + return
Loading player data...
; + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + return ( +
+

Player Information

+ {position && ( +
+

Position

+

X: {position.x}

+

Y: {position.y}

+
+ )} + +
+ ); +}; +``` + +### Enhanced Component + +```typescript +// src/components/PlayerInfoEnhanced.tsx +import React from 'react'; +import { usePlayerEnhanced } from '../hooks/usePlayerEnhanced'; + +export const PlayerInfoEnhanced: React.FC = () => { + const { position, moves, isLoading, error, refetch } = usePlayerEnhanced(); + + if (isLoading) { + return
Loading player data...
; + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + return ( +
+

Player Information

+ {position && ( +
+

Position

+

X: {position.x} (converted from hex)

+

Y: {position.y} (converted from hex)

+
+ )} + {moves && ( +
+

Moves

+

Remaining: {moves.remaining} (converted from hex)

+
+ )} + +
+ ); +}; +``` + +## 📋 Hook Best Practices + +### 1. Use useCallback for Fetch Functions + +```typescript +const fetchData = useCallback(async () => { + // ... fetch logic +}, [account?.address]); // Include dependencies +``` + +### 2. Handle Loading States Properly + +```typescript +const [isLoading, setIsLoading] = useState(true); +const [error, setError] = useState(null); + +// Always set loading to false in finally block +try { + // ... fetch logic +} catch (err) { + setError(err.message); +} finally { + setIsLoading(false); +} +``` + +### 3. Provide Refetch Function + +```typescript +const refetch = () => { + fetchData(); +}; + +return { data, isLoading, error, refetch }; +``` + +### 4. Use Proper Dependencies + +```typescript +useEffect(() => { + fetchData(); +}, [account?.address]); // Only depend on what you actually use +``` + +## 🎯 Hook Patterns Summary + +| Pattern | Use Case | Example | +|---------|----------|---------| +| **Basic Hook** | Simple data fetching | `usePlayer()` | +| **Multiple Model** | Complex state | `usePlayerState()` | +| **Enhanced Hook** | With data conversion | `usePlayerEnhanced()` | +| **List Hook** | Collections and lists | `useLeaderboard()` | +| **Auto-Refetch** | Real-time updates | `usePlayerAutoRefetch()` | + +## 📚 Next Steps + +1. **Learn data conversion** in the [Data Conversion](./data-conversion.md) guide +2. **Implement error handling** using the [Error Handling](./error-handling.md) guide +3. **Optimize performance** with the [Performance](./performance.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [Data Conversion](./data-conversion.md) From e6a7ec6cbddaf1ac7c087c5e12b471d696cdfac4 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:49:29 -0300 Subject: [PATCH 05/15] docs: add data conversion utilities guide for Torii integration - Add hex to number conversion utilities for Cairo model data - Include address formatting and validation utilities - Provide Cairo enum handling patterns (Direction enum examples) - Add bulk data conversion and type-safe conversion patterns - Include error handling and validation utilities for data conversion --- .../torii/guides/data-conversion.md | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/data-conversion.md diff --git a/client-new/pages/deployment/torii/guides/data-conversion.md b/client-new/pages/deployment/torii/guides/data-conversion.md new file mode 100644 index 0000000..427f6a3 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/data-conversion.md @@ -0,0 +1,388 @@ +# Data Conversion Utilities + +Learn how to convert Cairo model data types (hex values, addresses, enums) to JavaScript-friendly formats. + +## 🔧 Hex to Number Conversion + +### Basic Hex Conversion + +```typescript +// src/utils/dataConversion.ts + +// Convert Cairo u32/u64 hex values to JavaScript numbers +export const hexToNumber = (hexValue: string | number) => { + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + return Number(hexValue); +}; + +// Usage examples +const examples = { + u32: hexToNumber('0xa'), // 10 + u64: hexToNumber('0x64'), // 100 + string: hexToNumber('42'), // 42 (already a number) + invalid: hexToNumber('invalid'), // NaN +}; +``` + +### Safe Hex Conversion + +```typescript +// Safe conversion with fallback +export const safeHexToNumber = (hexValue: string | number, fallback: number = 0) => { + try { + const result = hexToNumber(hexValue); + return isNaN(result) ? fallback : result; + } catch { + return fallback; + } +}; + +// Usage +const position = { + x: safeHexToNumber(rawPosition.x, 0), + y: safeHexToNumber(rawPosition.y, 0) +}; +``` + +## 🎯 Address Formatting + +### ContractAddress Utilities + +```typescript +// Format ContractAddress for consistent queries +export const formatAddress = (address: string) => { + if (!address) return ''; + return address.toLowerCase().trim(); +}; + +// Validate Starknet address format +export const validateContractAddress = (address: string): boolean => { + if (!address) return false; + const addressRegex = /^0x[a-fA-F0-9]{63}$/; + return addressRegex.test(address); +}; + +// Sanitize address for queries +export const sanitizeAddress = (address: string): string => { + const formatted = formatAddress(address); + if (!validateContractAddress(formatted)) { + throw new Error(`Invalid contract address: ${address}`); + } + return formatted; +}; +``` + +## 🎲 Cairo Enum Handling + +### Direction Enum Conversion + +```typescript +// Handle Cairo enum types (e.g., Direction enum) +export const parseDirection = (directionValue: string | number) => { + const value = hexToNumber(directionValue); + switch (value) { + case 0: return 'None'; + case 1: return 'Left'; + case 2: return 'Right'; + case 3: return 'Up'; + case 4: return 'Down'; + default: return 'Unknown'; + } +}; + +// Reverse mapping +export const directionToNumber = (direction: string): number => { + switch (direction.toLowerCase()) { + case 'none': return 0; + case 'left': return 1; + case 'right': return 2; + case 'up': return 3; + case 'down': return 4; + default: return 0; + } +}; +``` + +### Generic Enum Parser + +```typescript +// Generic enum parser +export const parseEnum = (enumValue: string | number, enumMap: Record) => { + const value = hexToNumber(enumValue); + return enumMap[value] || 'Unknown'; +}; + +// Usage +const gameStateEnum = { + 0: 'Idle', + 1: 'Playing', + 2: 'Paused', + 3: 'GameOver' +}; + +const state = parseEnum('0x1', gameStateEnum); // 'Playing' +``` + +## 🔄 Bulk Data Conversion + +### Convert Multiple Hex Values + +```typescript +// Convert multiple hex values in an object +export const convertHexValues = (obj: any) => { + const converted = { ...obj }; + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'string' && value.startsWith('0x')) { + converted[key] = hexToNumber(value); + } + } + return converted; +}; + +// Usage +const rawPosition = { + player: '0x1234567890abcdef', + x: '0xa', + y: '0xb' +}; + +const position = convertHexValues(rawPosition); +// Result: { player: '0x1234567890abcdef', x: 10, y: 11 } +``` + +### Convert Array of Objects + +```typescript +// Convert hex values in array of objects +export const convertArrayHexValues = (array: any[]) => { + return array.map(item => convertHexValues(item)); +}; + +// Usage +const rawPlayers = [ + { player: '0x123...', remaining: '0x64' }, + { player: '0x456...', remaining: '0x32' } +]; + +const players = convertArrayHexValues(rawPlayers); +// Result: [{ player: '0x123...', remaining: 100 }, { player: '0x456...', remaining: 50 }] +``` + +## 🎯 Type-Safe Conversion + +### TypeScript Interfaces + +```typescript +// Define types for Cairo model data +interface RawPosition { + player: string; + x: string; + y: string; +} + +interface ConvertedPosition { + player: string; + x: number; + y: number; +} + +// Type-safe conversion function +export const convertPosition = (raw: RawPosition): ConvertedPosition => { + return { + player: formatAddress(raw.player), + x: hexToNumber(raw.x), + y: hexToNumber(raw.y) + }; +}; + +// Usage +const rawPosition: RawPosition = { + player: '0x1234567890abcdef', + x: '0xa', + y: '0xb' +}; + +const position: ConvertedPosition = convertPosition(rawPosition); +``` + +### Generic Type Converter + +```typescript +// Generic type converter +export const createConverter = , U extends Record>( + conversionMap: Record any> +) => { + return (data: T): U => { + const converted = {} as U; + for (const [key, converter] of Object.entries(conversionMap)) { + converted[key as keyof U] = converter(data[key as keyof T]); + } + return converted; + }; +}; + +// Usage +const positionConverter = createConverter({ + player: formatAddress, + x: hexToNumber, + y: hexToNumber +}); + +const position = positionConverter(rawPosition); +``` + +## 🛡️ Error Handling + +### Safe Conversion with Error Handling + +```typescript +// Safe conversion with detailed error handling +export const safeConvert = (converter: (value: any) => T, value: any, fieldName: string): T => { + try { + return converter(value); + } catch (error) { + console.error(`Error converting ${fieldName}:`, error); + throw new Error(`Failed to convert ${fieldName}: ${error.message}`); + } +}; + +// Usage +const position = { + x: safeConvert(hexToNumber, rawPosition.x, 'x'), + y: safeConvert(hexToNumber, rawPosition.y, 'y'), + player: safeConvert(formatAddress, rawPosition.player, 'player') +}; +``` + +### Validation Utilities + +```typescript +// Validate converted data +export const validatePosition = (position: any): boolean => { + return ( + typeof position.x === 'number' && + typeof position.y === 'number' && + validateContractAddress(position.player) + ); +}; + +// Usage +const convertedPosition = convertPosition(rawPosition); +if (!validatePosition(convertedPosition)) { + throw new Error('Invalid position data'); +} +``` + +## 🎮 Hook Integration + +### Hook with Data Conversion + +```typescript +// src/hooks/usePlayerWithConversion.ts +import { convertPosition, validatePosition } from '../utils/dataConversion'; + +export const usePlayerWithConversion = () => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: POSITION_QUERY, + variables: { playerOwner: account.address } + }) + }); + + const result = await response.json(); + const rawPosition = result.data?.fullStarterReactPositionModels?.edges[0]?.node; + + if (rawPosition) { + const convertedPosition = convertPosition(rawPosition); + if (validatePosition(convertedPosition)) { + setPosition(convertedPosition); + } else { + throw new Error('Invalid position data received'); + } + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + return { position, isLoading, error, refetch: fetchData }; +}; +``` + +## 📋 Best Practices + +### 1. Always Convert Hex Values + +```typescript +// ✅ Good: Convert hex values +const position = { + x: hexToNumber(rawPosition.x), + y: hexToNumber(rawPosition.y) +}; + +// ❌ Avoid: Using raw hex values +const position = { + x: rawPosition.x, // Still hex string + y: rawPosition.y // Still hex string +}; +``` + +### 2. Validate Addresses + +```typescript +// ✅ Good: Validate addresses +const player = sanitizeAddress(rawPosition.player); + +// ❌ Avoid: Using raw addresses +const player = rawPosition.player; // May be invalid +``` + +### 3. Handle Conversion Errors + +```typescript +// ✅ Good: Handle conversion errors +try { + const position = convertPosition(rawPosition); +} catch (error) { + console.error('Conversion failed:', error); + // Handle gracefully +} + +// ❌ Avoid: Ignoring conversion errors +const position = convertPosition(rawPosition); // May throw +``` + +## 📚 Next Steps + +1. **Implement error handling** using the [Error Handling](./error-handling.md) guide +2. **Optimize performance** with the [Performance](./performance.md) guide +3. **Add security measures** with the [Security](./security.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [Error Handling](./error-handling.md) From 3f8ccbe3685016d7c1d28d8a5890f875a2e025f8 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:49:43 -0300 Subject: [PATCH 06/15] docs: add error handling strategies guide for Torii integration - Add comprehensive error handling patterns for Torii queries - Include custom error classes (ToriiError, GraphQLError, NetworkError) - Provide retry strategies with exponential backoff - Add error boundaries and component error handling - Include error monitoring, logging, and recovery strategies --- .../deployment/torii/guides/error-handling.md | 576 ++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/error-handling.md diff --git a/client-new/pages/deployment/torii/guides/error-handling.md b/client-new/pages/deployment/torii/guides/error-handling.md new file mode 100644 index 0000000..4bc6405 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/error-handling.md @@ -0,0 +1,576 @@ +# Error Handling Strategies + +Learn how to implement robust error handling for Torii GraphQL queries and Cairo model data fetching. + +## 🛡️ Error Handling Overview + +Proper error handling is crucial for production applications. This guide covers: + +- GraphQL error handling +- Network error management +- Retry strategies with exponential backoff +- User-friendly error messages +- Error logging and monitoring + +## 🔧 Custom Error Classes + +### ToriiError Class + +```typescript +// src/utils/errorHandling.ts + +export class ToriiError extends Error { + constructor( + message: string, + public status?: number, + public code?: string, + public details?: any + ) { + super(message); + this.name = 'ToriiError'; + } +} + +// Specific error types +export class GraphQLError extends ToriiError { + constructor(message: string, details?: any) { + super(message, 400, 'GRAPHQL_ERROR', details); + this.name = 'GraphQLError'; + } +} + +export class NetworkError extends ToriiError { + constructor(message: string) { + super(message, 0, 'NETWORK_ERROR'); + this.name = 'NetworkError'; + } +} + +export class ValidationError extends ToriiError { + constructor(message: string, field?: string) { + super(message, 400, 'VALIDATION_ERROR', { field }); + this.name = 'ValidationError'; + } +} +``` + +### Error Handler Utility + +```typescript +// Comprehensive error handler +export const handleToriiError = (error: any): ToriiError => { + if (error.response) { + // GraphQL error response + const { errors } = error.response.data; + if (errors && errors.length > 0) { + return new GraphQLError( + errors[0].message, + errors[0].extensions || {} + ); + } + } + + if (error.request) { + // Network error + return new NetworkError( + 'Network error: Unable to connect to Torii' + ); + } + + // Generic error + return new ToriiError( + error.message || 'Unknown error occurred', + 500, + 'UNKNOWN_ERROR' + ); +}; +``` + +## 🔄 Enhanced Fetch Function + +### Torii Fetch with Error Handling + +```typescript +// Enhanced fetch function with comprehensive error handling +export const toriiFetch = async (query: string, variables: any) => { + try { + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }) + }); + + if (!response.ok) { + throw new ToriiError( + `HTTP ${response.status}: ${response.statusText}`, + response.status + ); + } + + const result = await response.json(); + + if (result.errors) { + throw new GraphQLError( + result.errors[0].message, + result.errors[0].extensions || {} + ); + } + + return result.data; + } catch (error) { + throw handleToriiError(error); + } +}; +``` + +## 🔁 Retry Strategies + +### Basic Retry Function + +```typescript +// Basic retry with exponential backoff +export const retryWithBackoff = async ( + fn: () => Promise, + maxAttempts: number = 3, + baseDelay: number = 1000 +): Promise => { + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + if (attempt === maxAttempts) { + throw error; + } + + // Exponential backoff: 1s, 2s, 4s, etc. + const delay = baseDelay * Math.pow(2, attempt - 1); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError!; +}; +``` + +### Advanced Retry with Conditions + +```typescript +// Advanced retry with specific error conditions +export const retryWithConditions = async ( + fn: () => Promise, + options: { + maxAttempts?: number; + baseDelay?: number; + retryableErrors?: string[]; + onRetry?: (attempt: number, error: Error) => void; + } = {} +): Promise => { + const { + maxAttempts = 3, + baseDelay = 1000, + retryableErrors = ['NETWORK_ERROR', 'TIMEOUT'], + onRetry + } = options; + + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + // Check if error is retryable + const isRetryable = retryableErrors.some(code => + error.code === code || error.message.includes(code) + ); + + if (attempt === maxAttempts || !isRetryable) { + throw error; + } + + // Call onRetry callback + if (onRetry) { + onRetry(attempt, error); + } + + // Exponential backoff with jitter + const delay = baseDelay * Math.pow(2, attempt - 1); + const jitter = Math.random() * 0.1 * delay; // 10% jitter + await new Promise(resolve => setTimeout(resolve, delay + jitter)); + } + } + + throw lastError!; +}; +``` + +## 🎣 Hook with Error Handling + +### Enhanced Hook with Retry + +```typescript +// src/hooks/usePlayerWithErrorHandling.ts +import { useEffect, useState } from "react"; +import { useAccount } from "@starknet-react/core"; +import { toriiFetch, retryWithBackoff, ToriiError } from "../utils/errorHandling"; + +export const usePlayerWithErrorHandling = () => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const { account } = useAccount(); + + const fetchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const data = await retryWithBackoff( + () => toriiFetch(POSITION_QUERY, { playerOwner: account.address }), + 3, // max attempts + 1000 // base delay + ); + + const positionData = data?.fullStarterReactPositionModels?.edges[0]?.node; + setPosition(positionData); + setRetryCount(0); + } catch (err) { + setError(err); + setRetryCount(prev => prev + 1); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + const refetch = () => { + setRetryCount(0); + fetchData(); + }; + + return { + position, + isLoading, + error, + retryCount, + refetch + }; +}; +``` + +## 🎮 Component Error Handling + +### Error Boundary Component + +```typescript +// src/components/ErrorBoundary.tsx +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+

Something went wrong

+

Please try refreshing the page

+ +
+ ); + } + + return this.props.children; + } +} +``` + +### Component with Error States + +```typescript +// src/components/PlayerInfoWithErrors.tsx +import React from 'react'; +import { usePlayerWithErrorHandling } from '../hooks/usePlayerWithErrorHandling'; +import { ToriiError } from '../utils/errorHandling'; + +export const PlayerInfoWithErrors: React.FC = () => { + const { position, isLoading, error, retryCount, refetch } = usePlayerWithErrorHandling(); + + if (isLoading) { + return ( +
+
Loading player data...
+ {retryCount > 0 && ( +
Retry attempt {retryCount}/3
+ )} +
+ ); + } + + if (error) { + return ( +
+

Error Loading Player Data

+ + {error instanceof ToriiError ? ( +
+

Error: {error.message}

+ {error.code &&

Code: {error.code}

} + {error.status &&

Status: {error.status}

} +
+ ) : ( +

An unexpected error occurred

+ )} + +
+ + +
+
+ ); + } + + return ( +
+

Player Information

+ {position && ( +
+

Position

+

X: {position.x}

+

Y: {position.y}

+
+ )} + +
+ ); +}; +``` + +## 📊 Error Monitoring + +### Error Logger + +```typescript +// src/utils/errorLogger.ts + +interface ErrorLog { + timestamp: string; + error: string; + code?: string; + status?: number; + details?: any; + userAgent: string; + url: string; +} + +export const logError = (error: ToriiError, context?: any) => { + const errorLog: ErrorLog = { + timestamp: new Date().toISOString(), + error: error.message, + code: error.code, + status: error.status, + details: { ...error.details, ...context }, + userAgent: navigator.userAgent, + url: window.location.href + }; + + // Log to console in development + if (process.env.NODE_ENV === 'development') { + console.error('Torii Error:', errorLog); + } + + // Send to error tracking service in production + if (process.env.NODE_ENV === 'production') { + // Example: send to Sentry, LogRocket, etc. + // errorTrackingService.captureException(error, errorLog); + } +}; +``` + +### Error Tracking Hook + +```typescript +// src/hooks/useErrorTracking.ts +import { useEffect } from 'react'; +import { logError } from '../utils/errorLogger'; + +export const useErrorTracking = (error: ToriiError | null, context?: any) => { + useEffect(() => { + if (error) { + logError(error, context); + } + }, [error, context]); +}; +``` + +## 🎯 Error Recovery Strategies + +### Graceful Degradation + +```typescript +// Hook with graceful degradation +export const usePlayerWithFallback = () => { + const [position, setPosition] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [useFallback, setUseFallback] = useState(false); + const { account } = useAccount(); + + const fetchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const data = await toriiFetch(POSITION_QUERY, { playerOwner: account.address }); + const positionData = data?.fullStarterReactPositionModels?.edges[0]?.node; + setPosition(positionData); + setUseFallback(false); + } catch (err) { + setError(err); + + // Use fallback data if available + if (localStorage.getItem('playerPosition')) { + const fallbackData = JSON.parse(localStorage.getItem('playerPosition')!); + setPosition(fallbackData); + setUseFallback(true); + } + } finally { + setIsLoading(false); + } + }; + + // Save successful data for fallback + useEffect(() => { + if (position && !useFallback) { + localStorage.setItem('playerPosition', JSON.stringify(position)); + } + }, [position, useFallback]); + + return { position, isLoading, error, useFallback, refetch: fetchData }; +}; +``` + +## 📋 Error Handling Best Practices + +### 1. Use Specific Error Types + +```typescript +// ✅ Good: Use specific error types +try { + const data = await toriiFetch(query, variables); +} catch (error) { + if (error instanceof NetworkError) { + // Handle network issues + } else if (error instanceof GraphQLError) { + // Handle GraphQL errors + } else { + // Handle unknown errors + } +} + +// ❌ Avoid: Generic error handling +try { + const data = await toriiFetch(query, variables); +} catch (error) { + // Generic handling - not helpful + console.error('Error:', error); +} +``` + +### 2. Implement Retry Logic + +```typescript +// ✅ Good: Implement retry with backoff +const data = await retryWithBackoff( + () => toriiFetch(query, variables), + 3, // max attempts + 1000 // base delay +); + +// ❌ Avoid: Simple retry without backoff +for (let i = 0; i < 3; i++) { + try { + return await toriiFetch(query, variables); + } catch (error) { + if (i === 2) throw error; + await new Promise(resolve => setTimeout(resolve, 1000)); + } +} +``` + +### 3. Provide User-Friendly Messages + +```typescript +// ✅ Good: User-friendly error messages +const getErrorMessage = (error: ToriiError) => { + switch (error.code) { + case 'NETWORK_ERROR': + return 'Unable to connect to the server. Please check your internet connection.'; + case 'GRAPHQL_ERROR': + return 'There was an issue with your request. Please try again.'; + case 'VALIDATION_ERROR': + return 'Invalid data provided. Please check your input.'; + default: + return 'An unexpected error occurred. Please try again.'; + } +}; + +// ❌ Avoid: Technical error messages +const getErrorMessage = (error: ToriiError) => { + return error.message; // Too technical for users +}; +``` + +## 📚 Next Steps + +1. **Optimize performance** with the [Performance](./performance.md) guide +2. **Add security measures** with the [Security](./security.md) guide +3. **Implement testing** using the [Testing](./testing.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [Performance](./performance.md) From f49e95b43aaed79491d4316e550fdce452dfaad8 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:49:55 -0300 Subject: [PATCH 07/15] docs: add performance optimization guide for Torii integration - Add caching strategies with TTL and LRU implementations - Include request batching and query deduplication patterns - Provide performance monitoring and metrics collection - Add memory management and query optimization techniques - Include performance best practices and optimization guidelines --- .../deployment/torii/guides/performance.md | 668 ++++++++++++++++++ 1 file changed, 668 insertions(+) create mode 100644 client-new/pages/deployment/torii/guides/performance.md diff --git a/client-new/pages/deployment/torii/guides/performance.md b/client-new/pages/deployment/torii/guides/performance.md new file mode 100644 index 0000000..cc298f0 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/performance.md @@ -0,0 +1,668 @@ +# Performance Optimization + +Learn how to optimize Torii data fetching performance with caching strategies, request batching, and other optimization techniques. + +## ⚡ Performance Overview + +Performance optimization is crucial for smooth user experiences. This guide covers: + +- Caching strategies with TTL +- Request batching and deduplication +- Memory management +- Query optimization +- Performance monitoring + +## 🗄️ Caching Strategies + +### Basic Cache Implementation + +```typescript +// src/utils/cache.ts + +interface CacheEntry { + data: any; + timestamp: number; + ttl: number; +} + +class ToriiCache { + private cache = new Map(); + private defaultTTL = 30000; // 30 seconds + + set(key: string, data: any, ttl: number = this.defaultTTL) { + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl + }); + } + + get(key: string): any | null { + const entry = this.cache.get(key); + if (!entry) return null; + + const isExpired = Date.now() - entry.timestamp > entry.ttl; + if (isExpired) { + this.cache.delete(key); + return null; + } + + return entry.data; + } + + clear() { + this.cache.clear(); + } + + size() { + return this.cache.size; + } +} + +export const toriiCache = new ToriiCache(); +``` + +### Cached Fetch Function + +```typescript +// Cached fetch function +export const cachedToriiFetch = async ( + query: string, + variables: any, + cacheKey: string, + ttl: number = 30000 +) => { + // Check cache first + const cached = toriiCache.get(cacheKey); + if (cached) { + return cached; + } + + // Fetch from Torii + const data = await toriiFetch(query, variables); + + // Cache the result + toriiCache.set(cacheKey, data, ttl); + + return data; +}; +``` + +### Advanced Cache with LRU + +```typescript +// src/utils/advancedCache.ts + +class LRUCache { + private capacity: number; + private cache = new Map(); + + constructor(capacity: number) { + this.capacity = capacity; + } + + get(key: K): V | undefined { + if (this.cache.has(key)) { + const value = this.cache.get(key)!; + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + return undefined; + } + + set(key: K, value: V): void { + if (this.cache.has(key)) { + this.cache.delete(key); + } else if (this.cache.size >= this.capacity) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + this.cache.set(key, value); + } + + clear(): void { + this.cache.clear(); + } + + size(): number { + return this.cache.size; + } +} + +// Advanced cache with LRU and TTL +class AdvancedToriiCache { + private cache = new LRUCache(100); // Max 100 entries + private defaultTTL = 30000; + + set(key: string, data: any, ttl: number = this.defaultTTL) { + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl + }); + } + + get(key: string): any | null { + const entry = this.cache.get(key); + if (!entry) return null; + + const isExpired = Date.now() - entry.timestamp > entry.ttl; + if (isExpired) { + this.cache.delete(key); + return null; + } + + return entry.data; + } +} + +export const advancedToriiCache = new AdvancedToriiCache(); +``` + +## 🎣 Optimized Hooks + +### Hook with Caching + +```typescript +// src/hooks/usePlayerOptimized.ts +import { useCallback } from 'react'; +import { cachedToriiFetch } from '../utils/cache'; + +export const usePlayerOptimized = () => { + const [position, setPosition] = useState(null); + const [moves, setMoves] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const PLAYER_STATE_QUERY = ` + query GetPlayerState($playerOwner: ContractAddress!) { + position: fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + } + } + } + moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { + edges { + node { + player + remaining + } + } + } + } + `; + + const fetchData = useCallback(async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const cacheKey = `player-${account.address}`; + + const data = await cachedToriiFetch( + PLAYER_STATE_QUERY, + { playerOwner: account.address }, + cacheKey, + 15000 // 15 second cache + ); + + if (data.position?.edges?.[0]?.node) { + setPosition(convertHexValues(data.position.edges[0].node)); + } + + if (data.moves?.edges?.[0]?.node) { + setMoves(convertHexValues(data.moves.edges[0].node)); + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }, [account?.address]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { position, moves, isLoading, error, refetch: fetchData }; +}; +``` + +## 🔄 Request Batching + +### Batch Multiple Queries + +```typescript +// src/utils/batchQueries.ts + +interface BatchQuery { + query: string; + variables: any; + cacheKey: string; +} + +export const batchToriiQueries = async (queries: BatchQuery[]) => { + // Group queries by cache key to avoid duplicates + const uniqueQueries = queries.filter((query, index, self) => + index === self.findIndex(q => q.cacheKey === query.cacheKey) + ); + + // Check cache for all queries first + const cachedResults = new Map(); + const uncachedQueries: BatchQuery[] = []; + + for (const query of uniqueQueries) { + const cached = toriiCache.get(query.cacheKey); + if (cached) { + cachedResults.set(query.cacheKey, cached); + } else { + uncachedQueries.push(query); + } + } + + // Fetch uncached queries in parallel + if (uncachedQueries.length > 0) { + const fetchPromises = uncachedQueries.map(async (query) => { + const data = await toriiFetch(query.query, query.variables); + toriiCache.set(query.cacheKey, data); + return { cacheKey: query.cacheKey, data }; + }); + + const results = await Promise.all(fetchPromises); + results.forEach(({ cacheKey, data }) => { + cachedResults.set(cacheKey, data); + }); + } + + // Return results in original order + return queries.map(query => cachedResults.get(query.cacheKey)); +}; +``` + +### Hook with Batching + +```typescript +// src/hooks/usePlayerBatch.ts +import { batchToriiQueries } from '../utils/batchQueries'; + +export const usePlayerBatch = () => { + const [playerData, setPlayerData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const fetchBatchData = async () => { + if (!account?.address) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const queries = [ + { + query: POSITION_QUERY, + variables: { playerOwner: account.address }, + cacheKey: `position-${account.address}` + }, + { + query: MOVES_QUERY, + variables: { playerOwner: account.address }, + cacheKey: `moves-${account.address}` + } + ]; + + const [positionData, movesData] = await batchToriiQueries(queries); + + setPlayerData({ + position: positionData?.fullStarterReactPositionModels?.edges[0]?.node, + moves: movesData?.fullStarterReactMovesModels?.edges[0]?.node + }); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchBatchData(); + }, [account?.address]); + + return { playerData, isLoading, error, refetch: fetchBatchData }; +}; +``` + +## 📊 Performance Monitoring + +### Performance Metrics + +```typescript +// src/utils/performance.ts + +interface PerformanceMetrics { + queryTime: number; + cacheHit: boolean; + dataSize: number; + timestamp: number; +} + +class PerformanceMonitor { + private metrics: PerformanceMetrics[] = []; + + recordMetric(metric: PerformanceMetrics) { + this.metrics.push(metric); + + // Keep only last 100 metrics + if (this.metrics.length > 100) { + this.metrics.shift(); + } + } + + getAverageQueryTime(): number { + if (this.metrics.length === 0) return 0; + const total = this.metrics.reduce((sum, m) => sum + m.queryTime, 0); + return total / this.metrics.length; + } + + getCacheHitRate(): number { + if (this.metrics.length === 0) return 0; + const hits = this.metrics.filter(m => m.cacheHit).length; + return hits / this.metrics.length; + } + + getMetrics(): PerformanceMetrics[] { + return [...this.metrics]; + } + + clear(): void { + this.metrics = []; + } +} + +export const performanceMonitor = new PerformanceMonitor(); +``` + +### Instrumented Fetch Function + +```typescript +// Instrumented fetch with performance monitoring +export const instrumentedToriiFetch = async ( + query: string, + variables: any, + cacheKey: string +) => { + const startTime = performance.now(); + let cacheHit = false; + + try { + // Check cache first + const cached = toriiCache.get(cacheKey); + if (cached) { + cacheHit = true; + const queryTime = performance.now() - startTime; + + performanceMonitor.recordMetric({ + queryTime, + cacheHit: true, + dataSize: JSON.stringify(cached).length, + timestamp: Date.now() + }); + + return cached; + } + + // Fetch from Torii + const data = await toriiFetch(query, variables); + + // Cache the result + toriiCache.set(cacheKey, data); + + const queryTime = performance.now() - startTime; + + performanceMonitor.recordMetric({ + queryTime, + cacheHit: false, + dataSize: JSON.stringify(data).length, + timestamp: Date.now() + }); + + return data; + } catch (error) { + const queryTime = performance.now() - startTime; + + performanceMonitor.recordMetric({ + queryTime, + cacheHit: false, + dataSize: 0, + timestamp: Date.now() + }); + + throw error; + } +}; +``` + +## 🎯 Query Optimization + +### Optimized GraphQL Queries + +```typescript +// Optimize queries by selecting only needed fields +const OPTIMIZED_POSITION_QUERY = ` + query GetPlayerPosition($playerOwner: ContractAddress!) { + fullStarterReactPositionModels(where: { player: $playerOwner }) { + edges { + node { + player + x + y + # Only select fields you actually use + } + } + } + } +`; + +// Use pagination for large datasets +const PAGINATED_PLAYERS_QUERY = ` + query GetPlayers($first: Int!, $after: String) { + fullStarterReactMovesModels( + first: $first, + after: $after, + orderBy: "remaining", + orderDirection: "desc" + ) { + edges { + node { + player + remaining + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } + } +`; +``` + +### Query Deduplication + +```typescript +// Prevent duplicate queries +class QueryDeduplicator { + private pendingQueries = new Map>(); + + async execute( + key: string, + queryFn: () => Promise + ): Promise { + if (this.pendingQueries.has(key)) { + return this.pendingQueries.get(key)!; + } + + const promise = queryFn().finally(() => { + this.pendingQueries.delete(key); + }); + + this.pendingQueries.set(key, promise); + return promise; + } + + clear(): void { + this.pendingQueries.clear(); + } +} + +export const queryDeduplicator = new QueryDeduplicator(); + +// Usage +const fetchPlayerData = async (playerAddress: string) => { + return queryDeduplicator.execute( + `player-${playerAddress}`, + () => toriiFetch(POSITION_QUERY, { playerOwner: playerAddress }) + ); +}; +``` + +## 🚀 Memory Management + +### Memory-Efficient Cache + +```typescript +// Memory-efficient cache with size limits +class MemoryEfficientCache { + private cache = new Map(); + private maxSize: number; + private maxMemoryUsage: number; + + constructor(maxSize: number = 100, maxMemoryUsage: number = 10 * 1024 * 1024) { // 10MB + this.maxSize = maxSize; + this.maxMemoryUsage = maxMemoryUsage; + } + + set(key: string, data: any, ttl: number = 30000) { + // Check memory usage + const currentMemory = this.getMemoryUsage(); + const newEntrySize = JSON.stringify(data).length; + + if (currentMemory + newEntrySize > this.maxMemoryUsage) { + this.evictOldest(); + } + + // Check size limit + if (this.cache.size >= this.maxSize) { + this.evictOldest(); + } + + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl + }); + } + + private evictOldest() { + let oldestKey: string | null = null; + let oldestTime = Date.now(); + + for (const [key, entry] of this.cache.entries()) { + if (entry.timestamp < oldestTime) { + oldestTime = entry.timestamp; + oldestKey = key; + } + } + + if (oldestKey) { + this.cache.delete(oldestKey); + } + } + + private getMemoryUsage(): number { + let total = 0; + for (const entry of this.cache.values()) { + total += JSON.stringify(entry.data).length; + } + return total; + } + + // ... other methods same as basic cache +} + +export const memoryEfficientCache = new MemoryEfficientCache(); +``` + +## 📋 Performance Best Practices + +### 1. Use Appropriate Cache TTL + +```typescript +// ✅ Good: Use different TTLs for different data types +const CACHE_TTL = { + POSITION: 5000, // 5 seconds - frequently updated + MOVES: 15000, // 15 seconds - moderately updated + LEADERBOARD: 60000 // 1 minute - rarely updated +}; + +// ❌ Avoid: Same TTL for all data +const CACHE_TTL = 30000; // Same for everything +``` + +### 2. Implement Request Batching + +```typescript +// ✅ Good: Batch multiple queries +const [position, moves] = await batchToriiQueries([ + { query: POSITION_QUERY, variables: { playerOwner }, cacheKey: `pos-${playerOwner}` }, + { query: MOVES_QUERY, variables: { playerOwner }, cacheKey: `moves-${playerOwner}` } +]); + +// ❌ Avoid: Separate requests +const position = await toriiFetch(POSITION_QUERY, { playerOwner }); +const moves = await toriiFetch(MOVES_QUERY, { playerOwner }); +``` + +### 3. Monitor Performance + +```typescript +// ✅ Good: Monitor and optimize based on metrics +const avgQueryTime = performanceMonitor.getAverageQueryTime(); +const cacheHitRate = performanceMonitor.getCacheHitRate(); + +if (avgQueryTime > 1000) { + console.warn('Slow queries detected'); +} + +if (cacheHitRate < 0.5) { + console.warn('Low cache hit rate'); +} + +// ❌ Avoid: No performance monitoring +// No visibility into performance issues +``` + +## 📚 Next Steps + +1. **Add security measures** with the [Security](./security.md) guide +2. **Implement testing** using the [Testing](./testing.md) guide +3. **Deploy to production** with the [Production](./production.md) guide + +--- + +**Back to**: [Torii Client Integration](../client-integration.md) | **Next**: [Security](./security.md) From e8bc24288f9e82d0c1ce0bc8be5d63f1092d0e26 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Fri, 29 Aug 2025 14:50:13 -0300 Subject: [PATCH 08/15] feat: add navigation for Torii integration guides - Add Torii Integration section to deployment navigation - Include links to all 6 detailed guides (setup, queries, hooks, etc.) - Add cross-reference in React Integration section - Update routes structure for proper navigation integration - Ensure all guides are discoverable in the documentation --- client-new/src/routes.ts | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/client-new/src/routes.ts b/client-new/src/routes.ts index 5658e35..3b14877 100644 --- a/client-new/src/routes.ts +++ b/client-new/src/routes.ts @@ -82,6 +82,10 @@ const sidebarConfig: SidebarItem[] = [ text: 'Components', link: '/guides/react/components', }, + { + text: 'Torii Client Integration', + link: '/deployment/torii/client-integration', + }, ], }, { @@ -123,6 +127,44 @@ const sidebarConfig: SidebarItem[] = [ text: 'Slot', link: '/deployment/slot', }, + { + text: 'Torii Integration', + items: [ + { + text: 'Client Integration', + link: '/deployment/torii/client-integration', + }, + { + text: 'Guides', + items: [ + { + text: 'Setup & Configuration', + link: '/deployment/torii/guides/setup', + }, + { + text: 'GraphQL Queries', + link: '/deployment/torii/guides/graphql-queries', + }, + { + text: 'Custom Hooks', + link: '/deployment/torii/guides/custom-hooks', + }, + { + text: 'Data Conversion', + link: '/deployment/torii/guides/data-conversion', + }, + { + text: 'Error Handling', + link: '/deployment/torii/guides/error-handling', + }, + { + text: 'Performance', + link: '/deployment/torii/guides/performance', + }, + ], + }, + ], + }, ], }, { From 8434e0dd703991d74411471a4d75e2dff1a5c650 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:06 -0300 Subject: [PATCH 09/15] docs: update main Torii client integration guide to use Player model - Replace Position/Moves examples with Player model (experience, health, coins, creation_day) - Update GraphQL queries to use fullStarterReactPlayerModels - Update component examples to display meaningful player statistics - Update data conversion examples to handle player data types - Improve prerequisites to reference Player model instead of Position/Moves This makes the documentation more meaningful and aligns with real-world Dojo Game Starter implementation patterns. --- .../deployment/torii/client-integration.md | 165 +++++++++++++----- 1 file changed, 119 insertions(+), 46 deletions(-) diff --git a/client-new/pages/deployment/torii/client-integration.md b/client-new/pages/deployment/torii/client-integration.md index d328147..c633c83 100644 --- a/client-new/pages/deployment/torii/client-integration.md +++ b/client-new/pages/deployment/torii/client-integration.md @@ -7,7 +7,7 @@ Learn how to consume Torii data from your React application using GraphQL querie ### Prerequisites - Torii indexer running with your deployed Dojo world - React application with `@starknet-react/core` installed -- Deployed Cairo models and systems (e.g., Position, Moves models) +- Deployed Cairo models and systems (e.g., Player model with experience, health, coins, creation_day) ### Basic Setup @@ -36,49 +36,110 @@ export const dojoConfig = createDojoConfig({ 4. **Create a basic hook**: ```typescript // src/hooks/usePlayer.ts -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useAccount } from "@starknet-react/core"; +import { addAddressPadding } from "starknet"; +import { dojoConfig } from "../dojoConfig"; + +interface Player { + owner: string; + experience: number; + health: number; + coins: number; + creation_day: number; +} + +const TORII_URL = dojoConfig.toriiUrl + "/graphql"; +const PLAYER_QUERY = ` + query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } + } +`; + +// Helper to convert hex values to numbers +const hexToNumber = (hexValue: string | number): number => { + if (typeof hexValue === 'number') return hexValue; + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; +}; export const usePlayer = () => { - const [position, setPosition] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const { account } = useAccount(); - // Fetch data from Torii + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + const fetchData = async () => { - if (!account?.address) return; - - const response = await fetch(`${dojoConfig.toriiUrl}/graphql`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: ` - query GetPlayerPosition($playerOwner: ContractAddress!) { - fullStarterReactPositionModels(where: { player: $playerOwner }) { - edges { - node { - player - x - y - } - } - } - } - `, - variables: { playerOwner: account.address } - }) - }); - - const result = await response.json(); - setPosition(result.data?.fullStarterReactPositionModels?.edges[0]?.node); - setIsLoading(false); + if (!userAddress) { + setIsLoading(false); + return; + } + + try { + setIsLoading(true); + setError(null); + + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: PLAYER_QUERY, + variables: { playerOwner: userAddress } + }) + }); + + const result = await response.json(); + + if (!result.data?.fullStarterReactPlayerModels?.edges?.length) { + setPlayer(null); + return; + } + + const rawPlayerData = result.data.fullStarterReactPlayerModels.edges[0].node; + const playerData: Player = { + owner: rawPlayerData.owner, + experience: hexToNumber(rawPlayerData.experience), + health: hexToNumber(rawPlayerData.health), + coins: hexToNumber(rawPlayerData.coins), + creation_day: hexToNumber(rawPlayerData.creation_day) + }; + + setPlayer(playerData); + } catch (err) { + setError(err instanceof Error ? err : new Error('Unknown error occurred')); + } finally { + setIsLoading(false); + } }; useEffect(() => { - fetchData(); - }, [account?.address]); + if (userAddress) { + fetchData(); + } + }, [userAddress]); - return { position, isLoading, refetch: fetchData }; + return { player, isLoading, error, refetch: fetchData }; }; ``` @@ -111,18 +172,24 @@ For comprehensive implementation details, explore these focused guides: import { usePlayer } from '../hooks/usePlayer'; export const PlayerInfo = () => { - const { position, isLoading, refetch } = usePlayer(); + const { player, isLoading, error, refetch } = usePlayer(); - if (isLoading) return
Loading...
; + if (isLoading) return
Loading player data...
; + if (error) return
Error: {error.message}
; return (
-

Player Position

- {position && ( +

Player Stats

+ {player ? (
-

X: {position.x}

-

Y: {position.y}

+

Owner: {player.owner}

+

Experience: {player.experience}

+

Health: {player.health}

+

Coins: {player.coins}

+

Creation Day: {player.creation_day}

+ ) : ( +

No player data found

)}
@@ -133,18 +200,24 @@ export const PlayerInfo = () => { ### Data Conversion ```typescript // Convert hex values from Cairo models -const hexToNumber = (hexValue) => { +const hexToNumber = (hexValue: string | number): number => { + if (typeof hexValue === 'number') return hexValue; if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { return parseInt(hexValue, 16); } - return hexValue; + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; }; -// Usage -const position = { - ...rawPosition, - x: hexToNumber(rawPosition.x), - y: hexToNumber(rawPosition.y) +// Usage with player data +const player = { + ...rawPlayerData, + experience: hexToNumber(rawPlayerData.experience), + health: hexToNumber(rawPlayerData.health), + coins: hexToNumber(rawPlayerData.coins), + creation_day: hexToNumber(rawPlayerData.creation_day) }; ``` From a679fcdd404fedf5a481c3f36d152422741bc12f Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:14 -0300 Subject: [PATCH 10/15] docs: update custom hooks guide to use Player model examples - Replace Position/Moves hook examples with Player model hooks - Update GraphQL queries to use fullStarterReactPlayerModels - Update multiple model hook to use Player model structure - Update enhanced hook with data conversion for player data - Maintain consistent Player interface across all hook examples This provides developers with practical, real-world hook patterns that match the Dojo Game Starter implementation. --- .../deployment/torii/guides/custom-hooks.md | 231 +++++++++++++----- 1 file changed, 168 insertions(+), 63 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/custom-hooks.md b/client-new/pages/deployment/torii/guides/custom-hooks.md index 87fe800..71b2c3a 100644 --- a/client-new/pages/deployment/torii/guides/custom-hooks.md +++ b/client-new/pages/deployment/torii/guides/custom-hooks.md @@ -8,34 +8,63 @@ Learn how to create custom React hooks for fetching and managing Cairo model dat ```typescript // src/hooks/usePlayer.ts -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useAccount } from "@starknet-react/core"; +import { addAddressPadding } from "starknet"; import { dojoConfig } from "../dojo/dojoConfig"; +interface Player { + owner: string; + experience: number; + health: number; + coins: number; + creation_day: number; +} + const TORII_URL = dojoConfig.toriiUrl + "/graphql"; -const POSITION_QUERY = ` - query GetPlayerPosition($playerOwner: ContractAddress!) { - fullStarterReactPositionModels(where: { player: $playerOwner }) { +const PLAYER_QUERY = ` + query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { edges { node { - player - x - y + owner + experience + health + coins + creation_day } } + totalCount } } `; +// Helper to convert hex values to numbers +const hexToNumber = (hexValue: string | number): number => { + if (typeof hexValue === 'number') return hexValue; + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; +}; + export const usePlayer = () => { - const [position, setPosition] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const { account } = useAccount(); + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + const fetchData = async () => { - if (!account?.address) { + if (!userAddress) { setIsLoading(false); return; } @@ -48,30 +77,46 @@ export const usePlayer = () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - query: POSITION_QUERY, - variables: { playerOwner: account.address } + query: PLAYER_QUERY, + variables: { playerOwner: userAddress } }) }); const result = await response.json(); - const positionData = result.data?.fullStarterReactPositionModels?.edges[0]?.node; - setPosition(positionData); + + if (!result.data?.fullStarterReactPlayerModels?.edges?.length) { + setPlayer(null); + return; + } + + const rawPlayerData = result.data.fullStarterReactPlayerModels.edges[0].node; + const playerData: Player = { + owner: rawPlayerData.owner, + experience: hexToNumber(rawPlayerData.experience), + health: hexToNumber(rawPlayerData.health), + coins: hexToNumber(rawPlayerData.coins), + creation_day: hexToNumber(rawPlayerData.creation_day) + }; + + setPlayer(playerData); } catch (err) { - setError(err.message); + setError(err instanceof Error ? err : new Error('Unknown error occurred')); } finally { setIsLoading(false); } }; useEffect(() => { - fetchData(); - }, [account?.address]); + if (userAddress) { + fetchData(); + } + }, [userAddress]); const refetch = () => { fetchData(); }; - return { position, isLoading, error, refetch }; + return { player, isLoading, error, refetch }; }; ``` @@ -86,32 +131,39 @@ import { useAccount } from "@starknet-react/core"; const PLAYER_STATE_QUERY = ` query GetPlayerState($playerOwner: ContractAddress!) { - position: fullStarterReactPositionModels(where: { player: $playerOwner }) { - edges { - node { - player - x - y - } - } - } - moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { + player: fullStarterReactPlayerModels(where: { owner: $playerOwner }) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } } } + # Add other model queries here as needed } `; export const usePlayerState = () => { - const [playerState, setPlayerState] = useState(null); + const [playerState, setPlayerState] = useState({ player: null }); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { account } = useAccount(); + // Helper to convert hex values to numbers + const hexToNumber = (hexValue: string | number) => { + if (typeof hexValue === 'number') return hexValue; + if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { + return parseInt(hexValue, 16); + } + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; + }; + const fetchPlayerState = async () => { if (!account?.address) { setIsLoading(false); @@ -132,7 +184,17 @@ export const usePlayerState = () => { }); const result = await response.json(); - setPlayerState(result.data); + + const rawPlayerData = result.data?.player?.edges[0]?.node; + const playerData = rawPlayerData ? { + owner: rawPlayerData.owner, + experience: hexToNumber(rawPlayerData.experience), + health: hexToNumber(rawPlayerData.health), + coins: hexToNumber(rawPlayerData.coins), + creation_day: hexToNumber(rawPlayerData.creation_day) + } : null; + + setPlayerState({ player: playerData }); } catch (err) { setError(err.message); } finally { @@ -154,30 +216,65 @@ export const usePlayerState = () => { ```typescript // src/hooks/usePlayerEnhanced.ts -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useAccount } from "@starknet-react/core"; +import { addAddressPadding } from "starknet"; +import { dojoConfig } from "../dojo/dojoConfig"; + +interface Player { + owner: string; + experience: number; + health: number; + coins: number; + creation_day: number; +} // Data conversion utilities -const hexToNumber = (hexValue: string | number) => { +const hexToNumber = (hexValue: string | number): number => { + if (typeof hexValue === 'number') return hexValue; if (typeof hexValue === 'string' && hexValue.startsWith('0x')) { return parseInt(hexValue, 16); } - return Number(hexValue); + if (typeof hexValue === 'string') { + return parseInt(hexValue, 10); + } + return 0; }; const formatAddress = (address: string) => { return address.toLowerCase(); }; +const TORII_URL = dojoConfig.toriiUrl + "/graphql"; +const PLAYER_QUERY = ` + query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + } + } +`; + export const usePlayerEnhanced = () => { - const [position, setPosition] = useState(null); - const [moves, setMoves] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const { account } = useAccount(); + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + const fetchData = async () => { - if (!account?.address) { + if (!userAddress) { setIsLoading(false); return; } @@ -186,44 +283,52 @@ export const usePlayerEnhanced = () => { setError(null); try { - const [positionData, movesData] = await Promise.all([ - fetchPlayerPosition(account.address), - fetchPlayerMoves(account.address) - ]); - - // Convert hex values to numbers - if (positionData) { - setPosition({ - ...positionData, - x: hexToNumber(positionData.x), - y: hexToNumber(positionData.y), - player: formatAddress(positionData.player) - }); - } + const response = await fetch(TORII_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: PLAYER_QUERY, + variables: { playerOwner: userAddress } + }) + }); - if (movesData) { - setMoves({ - ...movesData, - remaining: hexToNumber(movesData.remaining), - player: formatAddress(movesData.player) - }); + const result = await response.json(); + + if (!result.data?.fullStarterReactPlayerModels?.edges?.length) { + setPlayer(null); + return; } + + const rawPlayerData = result.data.fullStarterReactPlayerModels.edges[0].node; + + // Convert hex values to numbers and format data + const playerData: Player = { + owner: formatAddress(rawPlayerData.owner), + experience: hexToNumber(rawPlayerData.experience), + health: hexToNumber(rawPlayerData.health), + coins: hexToNumber(rawPlayerData.coins), + creation_day: hexToNumber(rawPlayerData.creation_day) + }; + + setPlayer(playerData); } catch (err) { - setError(err.message); + setError(err instanceof Error ? err : new Error('Unknown error occurred')); } finally { setIsLoading(false); } }; useEffect(() => { - fetchData(); - }, [account?.address]); + if (userAddress) { + fetchData(); + } + }, [userAddress]); const refetch = () => { fetchData(); }; - return { position, moves, isLoading, error, refetch }; + return { player, isLoading, error, refetch }; }; ``` From 8b6c8ec23e0ee341ddde016c81bf943180ce1759 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:21 -0300 Subject: [PATCH 11/15] docs: update GraphQL queries guide to use Player model - Replace Position/Moves query examples with Player model queries - Update query structure to use fullStarterReactPlayerModels - Update leaderboard query to sort by experience instead of remaining moves - Update filtered query to use health conditions - Update pagination query to use Player model fields - Update multiple model query to use Player model structure This provides developers with practical GraphQL query patterns that match real-world Dojo game implementations. --- .../torii/guides/graphql-queries.md | 101 +++++++++++------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/graphql-queries.md b/client-new/pages/deployment/torii/guides/graphql-queries.md index 022cfaf..2a585b9 100644 --- a/client-new/pages/deployment/torii/guides/graphql-queries.md +++ b/client-new/pages/deployment/torii/guides/graphql-queries.md @@ -23,35 +23,42 @@ query QueryName($variable: Type!) { ## 📊 Cairo Model Query Examples -### Position Model Query +### Player Model Query ```graphql -# Query for Position model (Cairo struct with #[key] player field) -query GetPlayerPosition($playerOwner: ContractAddress!) { - fullStarterReactPositionModels(where: { player: $playerOwner }) { +# Query for Player model (Cairo struct with #[key] owner field) +query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { edges { node { - player - x - y + owner + experience + health + coins + creation_day } } + totalCount } } ``` -### Moves Model Query +### Player Model Query with Multiple Fields ```graphql -# Query for Moves model (Cairo struct with #[key] player field) -query GetPlayerMoves($playerOwner: ContractAddress!) { - fullStarterReactMovesModels(where: { player: $playerOwner }) { +# Query for Player model with specific field filtering +query GetPlayerByExperience($minExperience: u32!) { + fullStarterReactPlayerModels(where: { experience: { gte: $minExperience } }) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } } + totalCount } } ``` @@ -61,23 +68,27 @@ query GetPlayerMoves($playerOwner: ContractAddress!) { ```graphql # Query multiple Cairo models for a player query GetPlayerState($playerOwner: ContractAddress!) { - position: fullStarterReactPositionModels(where: { player: $playerOwner }) { - edges { - node { - player - x - y - } - } - } - moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { + player: fullStarterReactPlayerModels(where: { owner: $playerOwner }) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } } } + # Add other model queries here as needed + # achievements: fullStarterReactAchievementModels(where: { player: $playerOwner }) { + # edges { + # node { + # player + # achievement_type + # unlocked_at + # } + # } + # } } ``` @@ -86,19 +97,23 @@ query GetPlayerState($playerOwner: ContractAddress!) { ### Leaderboard Query ```graphql -# Query top players by remaining moves +# Query top players by experience query GetTopPlayers { - fullStarterReactMovesModels( + fullStarterReactPlayerModels( first: 10, - orderBy: "remaining", + orderBy: "experience", orderDirection: "desc" ) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } } + totalCount } } ``` @@ -108,18 +123,22 @@ query GetTopPlayers { ```graphql # Query players with specific conditions query GetActivePlayers { - fullStarterReactMovesModels( - where: { remaining: { gt: "0" } }, + fullStarterReactPlayerModels( + where: { health: { gt: "0" } }, first: 50, - orderBy: "remaining", + orderBy: "experience", orderDirection: "desc" ) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } } + totalCount } } ``` @@ -129,16 +148,19 @@ query GetActivePlayers { ```graphql # Query with pagination query GetPlayersPage($first: Int!, $after: String) { - fullStarterReactMovesModels( + fullStarterReactPlayerModels( first: $first, after: $after, - orderBy: "remaining", + orderBy: "experience", orderDirection: "desc" ) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } cursor } @@ -148,6 +170,7 @@ query GetPlayersPage($first: Int!, $after: String) { startCursor endCursor } + totalCount } } ``` From e2f8f0fb61759a97b40c86d1b0cec4594930fcb5 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:28 -0300 Subject: [PATCH 12/15] docs: update data conversion guide to use Player model examples - Replace Position data conversion examples with Player model examples - Update hex conversion utilities to handle player data fields - Update bulk data conversion examples to use player data structure - Update type-safe conversion interfaces for Player model - Update generic type converter examples for player data - Maintain consistent Player interface across all conversion examples This provides developers with practical data conversion patterns for handling Cairo model data in real-world applications. --- .../torii/guides/data-conversion.md | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/data-conversion.md b/client-new/pages/deployment/torii/guides/data-conversion.md index 427f6a3..7a4dfde 100644 --- a/client-new/pages/deployment/torii/guides/data-conversion.md +++ b/client-new/pages/deployment/torii/guides/data-conversion.md @@ -39,10 +39,12 @@ export const safeHexToNumber = (hexValue: string | number, fallback: number = 0) } }; -// Usage -const position = { - x: safeHexToNumber(rawPosition.x, 0), - y: safeHexToNumber(rawPosition.y, 0) +// Usage with player data +const player = { + experience: safeHexToNumber(rawPlayer.experience, 0), + health: safeHexToNumber(rawPlayer.health, 100), + coins: safeHexToNumber(rawPlayer.coins, 0), + creation_day: safeHexToNumber(rawPlayer.creation_day, 0) }; ``` @@ -141,15 +143,17 @@ export const convertHexValues = (obj: any) => { return converted; }; -// Usage -const rawPosition = { - player: '0x1234567890abcdef', - x: '0xa', - y: '0xb' +// Usage with player data +const rawPlayer = { + owner: '0x1234567890abcdef', + experience: '0x64', + health: '0x32', + coins: '0x1e', + creation_day: '0x5' }; -const position = convertHexValues(rawPosition); -// Result: { player: '0x1234567890abcdef', x: 10, y: 11 } +const player = convertHexValues(rawPlayer); +// Result: { owner: '0x1234567890abcdef', experience: 100, health: 50, coins: 30, creation_day: 5 } ``` ### Convert Array of Objects @@ -160,14 +164,14 @@ export const convertArrayHexValues = (array: any[]) => { return array.map(item => convertHexValues(item)); }; -// Usage +// Usage with player data const rawPlayers = [ - { player: '0x123...', remaining: '0x64' }, - { player: '0x456...', remaining: '0x32' } + { owner: '0x123...', experience: '0x64', health: '0x32', coins: '0x1e', creation_day: '0x5' }, + { owner: '0x456...', experience: '0x32', health: '0x64', coins: '0x14', creation_day: '0x3' } ]; const players = convertArrayHexValues(rawPlayers); -// Result: [{ player: '0x123...', remaining: 100 }, { player: '0x456...', remaining: 50 }] +// Result: [{ owner: '0x123...', experience: 100, health: 50, coins: 30, creation_day: 5 }, { owner: '0x456...', experience: 50, health: 100, coins: 20, creation_day: 3 }] ``` ## 🎯 Type-Safe Conversion @@ -176,35 +180,43 @@ const players = convertArrayHexValues(rawPlayers); ```typescript // Define types for Cairo model data -interface RawPosition { - player: string; - x: string; - y: string; +interface RawPlayer { + owner: string; + experience: string; + health: string; + coins: string; + creation_day: string; } -interface ConvertedPosition { - player: string; - x: number; - y: number; +interface ConvertedPlayer { + owner: string; + experience: number; + health: number; + coins: number; + creation_day: number; } // Type-safe conversion function -export const convertPosition = (raw: RawPosition): ConvertedPosition => { +export const convertPlayer = (raw: RawPlayer): ConvertedPlayer => { return { - player: formatAddress(raw.player), - x: hexToNumber(raw.x), - y: hexToNumber(raw.y) + owner: formatAddress(raw.owner), + experience: hexToNumber(raw.experience), + health: hexToNumber(raw.health), + coins: hexToNumber(raw.coins), + creation_day: hexToNumber(raw.creation_day) }; }; // Usage -const rawPosition: RawPosition = { - player: '0x1234567890abcdef', - x: '0xa', - y: '0xb' +const rawPlayer: RawPlayer = { + owner: '0x1234567890abcdef', + experience: '0x64', + health: '0x32', + coins: '0x1e', + creation_day: '0x5' }; -const position: ConvertedPosition = convertPosition(rawPosition); +const player: ConvertedPlayer = convertPlayer(rawPlayer); ``` ### Generic Type Converter @@ -224,13 +236,15 @@ export const createConverter = , U extends Record< }; // Usage -const positionConverter = createConverter({ - player: formatAddress, - x: hexToNumber, - y: hexToNumber +const playerConverter = createConverter({ + owner: formatAddress, + experience: hexToNumber, + health: hexToNumber, + coins: hexToNumber, + creation_day: hexToNumber }); -const position = positionConverter(rawPosition); +const player = playerConverter(rawPlayer); ``` ## 🛡️ Error Handling From a78aadcfd0fc523d5572707d4edb0997037a4c5f Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:36 -0300 Subject: [PATCH 13/15] docs: update error handling guide to use Player model examples - Replace Position error handling examples with Player model examples - Update hook with error handling to use Player model structure - Update component usage example to display player statistics - Update fallback example to use player data instead of position data - Update GraphQL queries to use fullStarterReactPlayerModels - Maintain consistent error handling patterns for player data This provides developers with robust error handling patterns that work with real-world Player model implementations. --- .../deployment/torii/guides/error-handling.md | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/error-handling.md b/client-new/pages/deployment/torii/guides/error-handling.md index 4bc6405..ec68b42 100644 --- a/client-new/pages/deployment/torii/guides/error-handling.md +++ b/client-new/pages/deployment/torii/guides/error-handling.md @@ -220,7 +220,7 @@ import { useAccount } from "@starknet-react/core"; import { toriiFetch, retryWithBackoff, ToriiError } from "../utils/errorHandling"; export const usePlayerWithErrorHandling = () => { - const [position, setPosition] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [retryCount, setRetryCount] = useState(0); @@ -237,13 +237,13 @@ export const usePlayerWithErrorHandling = () => { try { const data = await retryWithBackoff( - () => toriiFetch(POSITION_QUERY, { playerOwner: account.address }), + () => toriiFetch(PLAYER_QUERY, { playerOwner: account.address }), 3, // max attempts 1000 // base delay ); - const positionData = data?.fullStarterReactPositionModels?.edges[0]?.node; - setPosition(positionData); + const playerData = data?.fullStarterReactPlayerModels?.edges[0]?.node; + setPlayer(playerData); setRetryCount(0); } catch (err) { setError(err); @@ -263,7 +263,7 @@ export const usePlayerWithErrorHandling = () => { }; return { - position, + player, isLoading, error, retryCount, @@ -331,7 +331,7 @@ import { usePlayerWithErrorHandling } from '../hooks/usePlayerWithErrorHandling' import { ToriiError } from '../utils/errorHandling'; export const PlayerInfoWithErrors: React.FC = () => { - const { position, isLoading, error, retryCount, refetch } = usePlayerWithErrorHandling(); + const { player, isLoading, error, retryCount, refetch } = usePlayerWithErrorHandling(); if (isLoading) { return ( @@ -372,11 +372,14 @@ export const PlayerInfoWithErrors: React.FC = () => { return (

Player Information

- {position && ( + {player && (
-

Position

-

X: {position.x}

-

Y: {position.y}

+

Player Stats

+

Owner: {player.owner}

+

Experience: {player.experience}

+

Health: {player.health}

+

Coins: {player.coins}

+

Creation Day: {player.creation_day}

)} @@ -449,7 +452,7 @@ export const useErrorTracking = (error: ToriiError | null, context?: any) => { ```typescript // Hook with graceful degradation export const usePlayerWithFallback = () => { - const [position, setPosition] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [useFallback, setUseFallback] = useState(false); @@ -465,17 +468,17 @@ export const usePlayerWithFallback = () => { setError(null); try { - const data = await toriiFetch(POSITION_QUERY, { playerOwner: account.address }); - const positionData = data?.fullStarterReactPositionModels?.edges[0]?.node; - setPosition(positionData); + const data = await toriiFetch(PLAYER_QUERY, { playerOwner: account.address }); + const playerData = data?.fullStarterReactPlayerModels?.edges[0]?.node; + setPlayer(playerData); setUseFallback(false); } catch (err) { setError(err); // Use fallback data if available - if (localStorage.getItem('playerPosition')) { - const fallbackData = JSON.parse(localStorage.getItem('playerPosition')!); - setPosition(fallbackData); + if (localStorage.getItem('playerData')) { + const fallbackData = JSON.parse(localStorage.getItem('playerData')!); + setPlayer(fallbackData); setUseFallback(true); } } finally { @@ -485,12 +488,12 @@ export const usePlayerWithFallback = () => { // Save successful data for fallback useEffect(() => { - if (position && !useFallback) { - localStorage.setItem('playerPosition', JSON.stringify(position)); + if (player && !useFallback) { + localStorage.setItem('playerData', JSON.stringify(player)); } - }, [position, useFallback]); + }, [player, useFallback]); - return { position, isLoading, error, useFallback, refetch: fetchData }; + return { player, isLoading, error, useFallback, refetch: fetchData }; }; ``` From 302a060154eaa1c83be69753f75d42f5788e1bd3 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:42 -0300 Subject: [PATCH 14/15] docs: update performance guide to use Player model examples - Replace Position/Moves performance examples with Player model examples - Update optimized hook to use Player model structure - Update request batching examples to use Player model queries - Update pagination query examples to use Player model fields - Update usage examples to demonstrate Player model optimization - Update GraphQL queries to use fullStarterReactPlayerModels This provides developers with performance optimization patterns that work with real-world Player model implementations. --- .../deployment/torii/guides/performance.md | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/performance.md b/client-new/pages/deployment/torii/guides/performance.md index cc298f0..fa825b8 100644 --- a/client-new/pages/deployment/torii/guides/performance.md +++ b/client-new/pages/deployment/torii/guides/performance.md @@ -170,30 +170,24 @@ import { useCallback } from 'react'; import { cachedToriiFetch } from '../utils/cache'; export const usePlayerOptimized = () => { - const [position, setPosition] = useState(null); - const [moves, setMoves] = useState(null); + const [player, setPlayer] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { account } = useAccount(); - const PLAYER_STATE_QUERY = ` - query GetPlayerState($playerOwner: ContractAddress!) { - position: fullStarterReactPositionModels(where: { player: $playerOwner }) { + const PLAYER_QUERY = ` + query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { edges { node { - player - x - y - } - } - } - moves: fullStarterReactMovesModels(where: { player: $playerOwner }) { - edges { - node { - player - remaining + owner + experience + health + coins + creation_day } } + totalCount } } `; @@ -211,18 +205,14 @@ export const usePlayerOptimized = () => { const cacheKey = `player-${account.address}`; const data = await cachedToriiFetch( - PLAYER_STATE_QUERY, + PLAYER_QUERY, { playerOwner: account.address }, cacheKey, 15000 // 15 second cache ); - if (data.position?.edges?.[0]?.node) { - setPosition(convertHexValues(data.position.edges[0].node)); - } - - if (data.moves?.edges?.[0]?.node) { - setMoves(convertHexValues(data.moves.edges[0].node)); + if (data.fullStarterReactPlayerModels?.edges?.[0]?.node) { + setPlayer(convertHexValues(data.fullStarterReactPlayerModels.edges[0].node)); } } catch (err) { setError(err.message); @@ -235,7 +225,7 @@ export const usePlayerOptimized = () => { fetchData(); }, [fetchData]); - return { position, moves, isLoading, error, refetch: fetchData }; + return { player, isLoading, error, refetch: fetchData }; }; ``` @@ -314,22 +304,16 @@ export const usePlayerBatch = () => { try { const queries = [ { - query: POSITION_QUERY, + query: PLAYER_QUERY, variables: { playerOwner: account.address }, - cacheKey: `position-${account.address}` - }, - { - query: MOVES_QUERY, - variables: { playerOwner: account.address }, - cacheKey: `moves-${account.address}` + cacheKey: `player-${account.address}` } ]; - const [positionData, movesData] = await batchToriiQueries(queries); + const [playerData] = await batchToriiQueries(queries); setPlayerData({ - position: positionData?.fullStarterReactPositionModels?.edges[0]?.node, - moves: movesData?.fullStarterReactMovesModels?.edges[0]?.node + player: playerData?.fullStarterReactPlayerModels?.edges[0]?.node }); } catch (err) { setError(err.message); @@ -480,16 +464,19 @@ const OPTIMIZED_POSITION_QUERY = ` // Use pagination for large datasets const PAGINATED_PLAYERS_QUERY = ` query GetPlayers($first: Int!, $after: String) { - fullStarterReactMovesModels( + fullStarterReactPlayerModels( first: $first, after: $after, - orderBy: "remaining", + orderBy: "experience", orderDirection: "desc" ) { edges { node { - player - remaining + owner + experience + health + coins + creation_day } cursor } @@ -497,6 +484,7 @@ const PAGINATED_PLAYERS_QUERY = ` hasNextPage endCursor } + totalCount } } `; @@ -628,14 +616,12 @@ const CACHE_TTL = 30000; // Same for everything ```typescript // ✅ Good: Batch multiple queries -const [position, moves] = await batchToriiQueries([ - { query: POSITION_QUERY, variables: { playerOwner }, cacheKey: `pos-${playerOwner}` }, - { query: MOVES_QUERY, variables: { playerOwner }, cacheKey: `moves-${playerOwner}` } +const [player] = await batchToriiQueries([ + { query: PLAYER_QUERY, variables: { playerOwner }, cacheKey: `player-${playerOwner}` } ]); // ❌ Avoid: Separate requests -const position = await toriiFetch(POSITION_QUERY, { playerOwner }); -const moves = await toriiFetch(MOVES_QUERY, { playerOwner }); +const player = await toriiFetch(PLAYER_QUERY, { playerOwner }); ``` ### 3. Monitor Performance From c4e0ea9ff74285923b2c08c1fd38dff1c16ddb66 Mon Sep 17 00:00:00 2001 From: Renzo Barcos Date: Tue, 9 Sep 2025 23:39:50 -0300 Subject: [PATCH 15/15] docs: update setup guide to use Player model examples - Replace Position/Moves prerequisites with Player model prerequisites - Update debug query example to use fullStarterReactPlayerModels - Update model verification examples to use Player model structure - Maintain consistent Player model references throughout setup guide This ensures the setup guide aligns with the Player model examples used throughout the rest of the Torii integration documentation. --- client-new/pages/deployment/torii/guides/setup.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client-new/pages/deployment/torii/guides/setup.md b/client-new/pages/deployment/torii/guides/setup.md index 36d33c6..239af5a 100644 --- a/client-new/pages/deployment/torii/guides/setup.md +++ b/client-new/pages/deployment/torii/guides/setup.md @@ -8,7 +8,7 @@ Before you begin, ensure you have: - **Torii indexer running** with your deployed Dojo world - **React application** with `@starknet-react/core` installed -- **Deployed Cairo models and systems** (e.g., Position, Moves models) +- **Deployed Cairo models and systems** (e.g., Player model with experience, health, coins, creation_day) - **Environment variables** configured for your network - **Dojo world address** and contract manifest @@ -142,14 +142,17 @@ const testConnection = async () => { // Debug query to check if models exist const DEBUG_QUERY = ` query DebugModels { - fullStarterReactPositionModels(first: 1) { + fullStarterReactPlayerModels(first: 1) { edges { node { - player - x - y + owner + experience + health + coins + creation_day } } + totalCount } } `;