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..c633c83 --- /dev/null +++ b/client-new/pages/deployment/torii/client-integration.md @@ -0,0 +1,241 @@ +# 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., Player model with experience, health, coins, creation_day) + +### 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, 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 [player, setPlayer] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + + const fetchData = async () => { + 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(() => { + if (userAddress) { + fetchData(); + } + }, [userAddress]); + + return { player, isLoading, error, 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 { player, isLoading, error, refetch } = usePlayer(); + + if (isLoading) return
Loading player data...
; + if (error) return
Error: {error.message}
; + + return ( +
+

Player Stats

+ {player ? ( +
+

Owner: {player.owner}

+

Experience: {player.experience}

+

Health: {player.health}

+

Coins: {player.coins}

+

Creation Day: {player.creation_day}

+
+ ) : ( +

No player data found

+ )} + +
+ ); +}; +``` + +### Data Conversion +```typescript +// Convert hex values from Cairo models +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; +}; + +// 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) +}; +``` + +## 🔗 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. 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..71b2c3a --- /dev/null +++ b/client-new/pages/deployment/torii/guides/custom-hooks.md @@ -0,0 +1,615 @@ +# 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, 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 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 [player, setPlayer] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + + const fetchData = async () => { + if (!userAddress) { + 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_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(() => { + if (userAddress) { + fetchData(); + } + }, [userAddress]); + + const refetch = () => { + fetchData(); + }; + + return { player, 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!) { + player: fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + } + # Add other model queries here as needed + } +`; + +export const usePlayerState = () => { + 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); + 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(); + + 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 { + 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, 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): 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 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 [player, setPlayer] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const userAddress = useMemo(() => + account ? addAddressPadding(account.address).toLowerCase() : '', + [account] + ); + + const fetchData = async () => { + if (!userAddress) { + 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_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; + + // 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 instanceof Error ? err : new Error('Unknown error occurred')); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (userAddress) { + fetchData(); + } + }, [userAddress]); + + const refetch = () => { + fetchData(); + }; + + return { player, 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) 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..7a4dfde --- /dev/null +++ b/client-new/pages/deployment/torii/guides/data-conversion.md @@ -0,0 +1,402 @@ +# 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 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) +}; +``` + +## 🎯 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 with player data +const rawPlayer = { + owner: '0x1234567890abcdef', + experience: '0x64', + health: '0x32', + coins: '0x1e', + creation_day: '0x5' +}; + +const player = convertHexValues(rawPlayer); +// Result: { owner: '0x1234567890abcdef', experience: 100, health: 50, coins: 30, creation_day: 5 } +``` + +### Convert Array of Objects + +```typescript +// Convert hex values in array of objects +export const convertArrayHexValues = (array: any[]) => { + return array.map(item => convertHexValues(item)); +}; + +// Usage with player data +const rawPlayers = [ + { 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: [{ 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 + +### TypeScript Interfaces + +```typescript +// Define types for Cairo model data +interface RawPlayer { + owner: string; + experience: string; + health: string; + coins: string; + creation_day: string; +} + +interface ConvertedPlayer { + owner: string; + experience: number; + health: number; + coins: number; + creation_day: number; +} + +// Type-safe conversion function +export const convertPlayer = (raw: RawPlayer): ConvertedPlayer => { + return { + owner: formatAddress(raw.owner), + experience: hexToNumber(raw.experience), + health: hexToNumber(raw.health), + coins: hexToNumber(raw.coins), + creation_day: hexToNumber(raw.creation_day) + }; +}; + +// Usage +const rawPlayer: RawPlayer = { + owner: '0x1234567890abcdef', + experience: '0x64', + health: '0x32', + coins: '0x1e', + creation_day: '0x5' +}; + +const player: ConvertedPlayer = convertPlayer(rawPlayer); +``` + +### 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 playerConverter = createConverter({ + owner: formatAddress, + experience: hexToNumber, + health: hexToNumber, + coins: hexToNumber, + creation_day: hexToNumber +}); + +const player = playerConverter(rawPlayer); +``` + +## 🛡️ 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) 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..ec68b42 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/error-handling.md @@ -0,0 +1,579 @@ +# 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 [player, setPlayer] = 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(PLAYER_QUERY, { playerOwner: account.address }), + 3, // max attempts + 1000 // base delay + ); + + const playerData = data?.fullStarterReactPlayerModels?.edges[0]?.node; + setPlayer(playerData); + setRetryCount(0); + } catch (err) { + setError(err); + setRetryCount(prev => prev + 1); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [account?.address]); + + const refetch = () => { + setRetryCount(0); + fetchData(); + }; + + return { + player, + 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 { player, 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

+ {player && ( +
+

Player Stats

+

Owner: {player.owner}

+

Experience: {player.experience}

+

Health: {player.health}

+

Coins: {player.coins}

+

Creation Day: {player.creation_day}

+
+ )} + +
+ ); +}; +``` + +## 📊 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 [player, setPlayer] = 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(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('playerData')) { + const fallbackData = JSON.parse(localStorage.getItem('playerData')!); + setPlayer(fallbackData); + setUseFallback(true); + } + } finally { + setIsLoading(false); + } + }; + + // Save successful data for fallback + useEffect(() => { + if (player && !useFallback) { + localStorage.setItem('playerData', JSON.stringify(player)); + } + }, [player, useFallback]); + + return { player, 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) 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..2a585b9 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/graphql-queries.md @@ -0,0 +1,412 @@ +# 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 + +### Player Model Query + +```graphql +# Query for Player model (Cairo struct with #[key] owner field) +query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } +} +``` + +### Player Model Query with Multiple Fields + +```graphql +# Query for Player model with specific field filtering +query GetPlayerByExperience($minExperience: u32!) { + fullStarterReactPlayerModels(where: { experience: { gte: $minExperience } }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } +} +``` + +### Multiple Model Query + +```graphql +# Query multiple Cairo models for a player +query GetPlayerState($playerOwner: ContractAddress!) { + player: fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + 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 + # } + # } + # } +} +``` + +## 🎯 Advanced Query Patterns + +### Leaderboard Query + +```graphql +# Query top players by experience +query GetTopPlayers { + fullStarterReactPlayerModels( + first: 10, + orderBy: "experience", + orderDirection: "desc" + ) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } +} +``` + +### Filtered Query + +```graphql +# Query players with specific conditions +query GetActivePlayers { + fullStarterReactPlayerModels( + where: { health: { gt: "0" } }, + first: 50, + orderBy: "experience", + orderDirection: "desc" + ) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } +} +``` + +### Pagination Query + +```graphql +# Query with pagination +query GetPlayersPage($first: Int!, $after: String) { + fullStarterReactPlayerModels( + first: $first, + after: $after, + orderBy: "experience", + orderDirection: "desc" + ) { + edges { + node { + owner + experience + health + coins + creation_day + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } +} +``` + +## 🔧 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) 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..fa825b8 --- /dev/null +++ b/client-new/pages/deployment/torii/guides/performance.md @@ -0,0 +1,654 @@ +# 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 [player, setPlayer] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { account } = useAccount(); + + const PLAYER_QUERY = ` + query GetPlayer($playerOwner: ContractAddress!) { + fullStarterReactPlayerModels(where: { owner: $playerOwner }) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } + } + `; + + 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_QUERY, + { playerOwner: account.address }, + cacheKey, + 15000 // 15 second cache + ); + + if (data.fullStarterReactPlayerModels?.edges?.[0]?.node) { + setPlayer(convertHexValues(data.fullStarterReactPlayerModels.edges[0].node)); + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }, [account?.address]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { player, 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: PLAYER_QUERY, + variables: { playerOwner: account.address }, + cacheKey: `player-${account.address}` + } + ]; + + const [playerData] = await batchToriiQueries(queries); + + setPlayerData({ + player: playerData?.fullStarterReactPlayerModels?.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) { + fullStarterReactPlayerModels( + first: $first, + after: $after, + orderBy: "experience", + orderDirection: "desc" + ) { + edges { + node { + owner + experience + health + coins + creation_day + } + cursor + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + } +`; +``` + +### 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 [player] = await batchToriiQueries([ + { query: PLAYER_QUERY, variables: { playerOwner }, cacheKey: `player-${playerOwner}` } +]); + +// ❌ Avoid: Separate requests +const player = await toriiFetch(PLAYER_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) 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..239af5a --- /dev/null +++ b/client-new/pages/deployment/torii/guides/setup.md @@ -0,0 +1,205 @@ +# 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., Player model with experience, health, coins, creation_day) +- **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 { + fullStarterReactPlayerModels(first: 1) { + edges { + node { + owner + experience + health + coins + creation_day + } + } + totalCount + } + } +`; +``` + +### 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) 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', + }, + ], + }, + ], + }, ], }, {