UNPKG

@shogun-sdk/one-shot

Version:

Shogun SDK - One Shot: React Components and hooks for cross-chain swaps

954 lines (799 loc) 27.3 kB
# @shogun-sdk/one-shot React hooks and components for cross-chain swap interfaces with support for both EVM and Solana chains. ## Quick Start ### 1. Install the Package Choose your preferred package manager: **npm** ```bash npm install @shogun-sdk/one-shot ``` **pnpm** ```bash pnpm add @shogun-sdk/one-shot ``` **yarn** ```bash yarn add @shogun-sdk/one-shot ``` ### 2. Set Up Providers and Build Your Swap Interface Wrap your app with providers and start building: ```typescript 'use client'; import { ShogunBalancesProvider, ShogunQuoteProvider } from '@shogun-sdk/one-shot'; import { useShogunQuote, useTokenBalances } from '@shogun-sdk/one-shot'; import { useState } from 'react'; // 1. Set up providers at your app root export function ShogunProvider({ children }: { children: React.ReactNode }) { const api = { key: process.env.SHOGUN_API_KEY!, url: process.env.SHOGUN_API_URL! }; const affiliateWallets = { solana: process.env.SOLANA_AFFILIATE_WALLET!, evm: process.env.EVM_AFFILIATE_WALLET! }; return ( <ShogunBalancesProvider apiKey={process.env.CODEX_API_KEY!}> <ShogunQuoteProvider swap={{ tokenIn: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18, chainId: 1 }, tokenOut: { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, chainId: 8453 }, setLatestSuggestedAutoSlippageValue: (value) => console.log('Slippage:', value), inputAmount: '1000000000000000000', // 1 ETH setInputAmount: (amount) => console.log('Amount:', amount), recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', slippage: 0.5, dynamicSlippage: false, }} system={{ api, systemFeePercent: 0.01, userEVMAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', userSolanaAddress: 'YourSolanaAddress', affiliateWallets, }} > {children} </ShogunQuoteProvider> </ShogunBalancesProvider> ); } // 2. Build your swap component function SwapInterface() { const [amount, setAmount] = useState('1000000000000000000'); // Get quote data const { quotes, isLoading, errors, fees, handleMaxBalanceInput, inputValue, setInputValue } = useShogunQuote(); // Get token balances const { evmBalances, isLoadingEVM } = useTokenBalances({ userEVMAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', userSolanaAddress: '', }); if (isLoading) return <div>Loading quote...</div>; if (errors.balanceError) return <div>Error: {errors.balanceError}</div>; return ( <div className="swap-interface"> <h2>Cross-Chain Swap</h2> <div className="input-section"> <label>Amount (ETH)</label> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="0.0" /> <button onClick={() => handleMaxBalanceInput(quotes, fees, true)}> MAX </button> </div> {quotes && ( <div className="quote-info"> <p>Output: {quotes.outputAmount}</p> <p>Rate: 1 ETH = {quotes.rate} USDC</p> <button onClick={() => console.log('Execute swap:', quotes)}> Swap Now </button> </div> )} </div> ); } // 3. Use in your app export default function App() { return ( <ShogunProvider> <SwapInterface /> </ShogunProvider> ); } ``` ### 3. Explore Advanced Features Check the sections below for comprehensive examples: - [Wagmi Integration](#wagmi-integration) - Connect wallets and handle transactions - [Advanced Components](#advanced-components) - Complete swap interfaces - [Hooks Reference](#hooks-reference) - All available hooks and their usage - [Best Practices](#best-practices) - Optimization and error handling ## Features - 🔄 **Cross-Chain Swaps** - Swap between EVM chains and Solana - 💰 **Real-Time Balances** - Live token balance tracking across chains - 📊 **Dynamic Quotes** - Real-time pricing with automatic updates - 🎯 **Smart Slippage** - Automatic slippage calculation and optimization - 💸 **Fee Management** - Built-in fee validation and affiliate support - ⚡ **React Query** - Efficient data fetching and caching - 🛡️ **TypeScript** - Full type safety and IntelliSense support - 🎨 **Headless UI** - Unstyled components for maximum customization ## Core Concepts ### Providers Architecture The SDK uses a two-provider architecture for optimal performance: 1. **ShogunBalancesProvider** - Manages token balances across all supported chains 2. **ShogunQuoteProvider** - Handles swap quotes, fees, and transaction logic ### Key Hooks - **useShogunQuote** - Access quote data, loading states, and swap operations - **useShogunBalances** - Direct access to the balance client for custom queries - **useTokenBalances** - Simplified hook for getting specific token balances ### State Management The SDK manages complex cross-chain state internally while exposing simple interfaces: - Automatic quote refetching based on input changes - Balance updates on wallet/network changes - Fee calculations with affiliate support - Error handling and validation ## Wagmi Integration ### Basic Wallet Connection ```typescript import { useAccount, useConnect, useDisconnect } from 'wagmi'; import { InjectedConnector } from 'wagmi/connectors/injected'; import { useShogunQuote } from '@shogun-sdk/one-shot'; function WalletConnect() { const { address, isConnected } = useAccount(); const { connect } = useConnect({ connector: new InjectedConnector(), }); const { disconnect } = useDisconnect(); return ( <div className="wallet-section"> {isConnected ? ( <div> <p>Connected: {address}</p> <button onClick={() => disconnect()}>Disconnect</button> </div> ) : ( <button onClick={() => connect()}>Connect Wallet</button> )} </div> ); } ``` ### Complete Swap Interface with Wagmi ```typescript import { useAccount, useConnect, useDisconnect, useNetwork } from 'wagmi'; import { InjectedConnector } from 'wagmi/connectors/injected'; import { useShogunQuote, useTokenBalances } from '@shogun-sdk/one-shot'; import { useState, useEffect } from 'react'; function AdvancedSwapInterface() { const { address, isConnected } = useAccount(); const { connect } = useConnect({ connector: new InjectedConnector(), }); const { disconnect } = useDisconnect(); const { chain } = useNetwork(); const [amount, setAmount] = useState('1000000000000000000'); // 1 ETH const [selectedTokenIn, setSelectedTokenIn] = useState({ address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH decimals: 18, chainId: 1, symbol: 'WETH' }); const [selectedTokenOut, setSelectedTokenOut] = useState({ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base decimals: 6, chainId: 8453, symbol: 'USDC' }); // Get quote with connected wallet const { quotes, isLoading: quoteLoading, errors, fees, handleMaxBalanceInput, inputValue, setInputValue } = useShogunQuote(); // Get balances for connected wallet const { evmBalances, solanaBalances, isLoadingEVM, isLoadingSolana } = useTokenBalances({ userEVMAddress: address || '', userSolanaAddress: '', // Add your Solana address if needed tokenIn: selectedTokenIn, tokenOut: selectedTokenOut }); // Handle max button click const handleMaxClick = () => { handleMaxBalanceInput(quotes, fees, true); }; if (!isConnected) { return ( <div className="wallet-connect"> <h2>Connect Your Wallet</h2> <p>Connect your wallet to start swapping tokens across chains</p> <button onClick={() => connect()}>Connect Wallet</button> </div> ); } return ( <div className="advanced-swap-interface"> {/* Wallet Info */} <div className="wallet-info"> <div className="wallet-details"> <p>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</p> <p>Network: {chain?.name}</p> </div> <button onClick={() => disconnect()}>Disconnect</button> </div> {/* Token Selection */} <div className="token-selection"> <div className="input-token"> <h3>From</h3> <select value={selectedTokenIn.address} onChange={(e) => { const token = tokenOptions.find(t => t.address === e.target.value); if (token) setSelectedTokenIn(token); }} > <option value="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2">WETH (Ethereum)</option> <option value="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48">USDC (Ethereum)</option> </select> <div className="amount-input"> <input type="number" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="0.0" min="0" step="0.000000000000000001" /> <button onClick={handleMaxClick} disabled={isLoadingEVM}> {isLoadingEVM ? 'Loading...' : 'MAX'} </button> </div> <p className="balance"> Balance: {evmBalances?.[selectedTokenIn.address] || '0'} {selectedTokenIn.symbol} </p> </div> <div className="swap-arrow">↓</div> <div className="output-token"> <h3>To</h3> <select value={selectedTokenOut.address} onChange={(e) => { const token = tokenOptions.find(t => t.address === e.target.value); if (token) setSelectedTokenOut(token); }} > <option value="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913">USDC (Base)</option> <option value="0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8">USDC (Arbitrum)</option> </select> <div className="output-amount"> <span>{quotes?.outputAmount || '0'}</span> <span className="symbol">{selectedTokenOut.symbol}</span> </div> <p className="balance"> Balance: {evmBalances?.[selectedTokenOut.address] || '0'} {selectedTokenOut.symbol} </p> </div> </div> {/* Quote Information */} {quotes && ( <div className="quote-details"> <div className="rate"> <span>Rate: 1 {selectedTokenIn.symbol} = {quotes.rate} {selectedTokenOut.symbol}</span> </div> <div className="fees"> <span>Network Fee: {fees?.networkFee || '0'}</span> <span>Protocol Fee: {fees?.protocolFee || '0'}</span> </div> </div> )} {/* Error Display */} {errors.balanceError && ( <div className="error"> <p>Balance Error: {errors.balanceError}</p> </div> )} {errors.feeValidationError && ( <div className="error"> <p>Fee Error: {errors.feeValidationError}</p> </div> )} {/* Swap Button */} <button className="swap-button" onClick={() => { console.log('Executing swap:', quotes); // Implement swap execution logic here }} disabled={!quotes || quoteLoading || !!errors.balanceError || !!errors.feeValidationError} > {quoteLoading ? 'Getting Quote...' : errors.balanceError ? 'Insufficient Balance' : errors.feeValidationError ? 'Fee Error' : 'Swap Tokens'} </button> </div> ); } // Token options for the example const tokenOptions = [ { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18, chainId: 1, symbol: 'WETH' }, { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6, chainId: 1, symbol: 'USDC' }, { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, chainId: 8453, symbol: 'USDC' }, { address: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', decimals: 6, chainId: 42161, symbol: 'USDC' }, ]; ``` ## Advanced Components ### Custom Provider with State Management ```typescript import { ShogunBalancesProvider, ShogunQuoteProvider } from '@shogun-sdk/one-shot'; import { createContext, useContext, useState, useMemo } from 'react'; // Custom swap state management interface SwapState { tokenIn: Token; tokenOut: Token; inputAmount: string; slippage: number; recipientAddress: string; } const SwapStateContext = createContext<{ state: SwapState; updateState: (updates: Partial<SwapState>) => void; } | null>(null); export function CustomShogunProvider({ children }: { children: React.ReactNode }) { const [swapState, setSwapState] = useState<SwapState>({ tokenIn: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18, chainId: 1 }, tokenOut: { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, chainId: 8453 }, inputAmount: '0', slippage: 0.5, recipientAddress: '' }); const updateState = (updates: Partial<SwapState>) => { setSwapState(prev => ({ ...prev, ...updates })); }; // Memoize configurations const api = useMemo(() => ({ key: process.env.SHOGUN_API_KEY!, url: process.env.SHOGUN_API_URL! }), []); const affiliateWallets = useMemo(() => ({ solana: process.env.SOLANA_AFFILIATE_WALLET!, evm: process.env.EVM_AFFILIATE_WALLET! }), []); return ( <SwapStateContext.Provider value={{ state: swapState, updateState }}> <ShogunBalancesProvider apiKey={process.env.CODEX_API_KEY!}> <ShogunQuoteProvider swap={{ tokenIn: swapState.tokenIn, tokenOut: swapState.tokenOut, setLatestSuggestedAutoSlippageValue: (value) => updateState({ slippage: value }), inputAmount: swapState.inputAmount, setInputAmount: (amount) => updateState({ inputAmount: amount }), recipientAddress: swapState.recipientAddress, slippage: swapState.slippage, dynamicSlippage: false, }} system={{ api, systemFeePercent: 0.01, userEVMAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', userSolanaAddress: 'YourSolanaAddress', affiliateWallets, notifyAboutError: (error) => console.error('Shogun SDK Error:', error) }} > {children} </ShogunQuoteProvider> </ShogunBalancesProvider> </SwapStateContext.Provider> ); } // Hook to use swap state export function useSwapState() { const context = useContext(SwapStateContext); if (!context) { throw new Error('useSwapState must be used within CustomShogunProvider'); } return context; } ``` ### Multi-Chain Balance Display ```typescript import { useTokenBalances, useShogunBalances } from '@shogun-sdk/one-shot'; import { useState, useEffect } from 'react'; function MultiChainBalanceDisplay({ userEVMAddress, userSolanaAddress }: { userEVMAddress: string; userSolanaAddress: string; }) { const [selectedTokens, setSelectedTokens] = useState([ { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18, chainId: 1, symbol: 'WETH' }, { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6, chainId: 1, symbol: 'USDC' }, ]); const { evmBalances, solanaBalances, isLoadingEVM, isLoadingSolana } = useTokenBalances({ userEVMAddress, userSolanaAddress, }); const balancesClient = useShogunBalances(); // Calculate total portfolio value const [totalValue, setTotalValue] = useState(0); useEffect(() => { const calculateTotalValue = async () => { let total = 0; // Calculate EVM balances value if (evmBalances) { for (const [tokenAddress, balance] of Object.entries(evmBalances)) { try { const price = await balancesClient.getTokenUSDPrice(tokenAddress, 1); total += parseFloat(balance) * price.usd; } catch (error) { console.error('Error fetching price for', tokenAddress, error); } } } setTotalValue(total); }; calculateTotalValue(); }, [evmBalances, balancesClient]); if (isLoadingEVM || isLoadingSolana) { return <div className="loading">Loading balances...</div>; } return ( <div className="balance-display"> <h2>Portfolio Overview</h2> <div className="total-value"> <h3>Total Value: ${totalValue.toFixed(2)}</h3> </div> <div className="chain-balances"> <div className="evm-balances"> <h4>EVM Chains</h4> {evmBalances && Object.entries(evmBalances).map(([tokenAddress, balance]) => { const token = selectedTokens.find(t => t.address === tokenAddress); return ( <div key={tokenAddress} className="balance-item"> <span>{token?.symbol || tokenAddress.slice(0, 6)}</span> <span>{parseFloat(balance).toFixed(6)}</span> </div> ); })} </div> <div className="solana-balances"> <h4>Solana</h4> {solanaBalances && Object.entries(solanaBalances).map(([tokenAddress, balance]) => ( <div key={tokenAddress} className="balance-item"> <span>{tokenAddress.slice(0, 6)}...</span> <span>{parseFloat(balance).toFixed(6)}</span> </div> ))} </div> </div> </div> ); } ``` ## Hooks Reference ### useShogunQuote The main hook for accessing quote data and swap operations. ```typescript const { quotes, // Current quote data errors, // Error states (feeValidationError, balanceError) quoteRefetch, // Function to manually refetch quote fees, // Fee breakdown isMaxBtnClicked, // Whether max button was clicked handleMaxBalanceInput, // Function to handle max balance input setIsMaxBtnClicked, // Set max button state inputValue, // Current input value setInputValue, // Set input value needRecalculateMaxValue, // Whether max value needs recalculation isLoading, // Loading state isRefetching, // Refetching state userInputAddress, // User's input token address userOutputAddress // User's output token address } = useShogunQuote(); ``` ### useTokenBalances Simplified hook for getting token balances. ```typescript const { evmBalances, // EVM token balances object solanaBalances, // Solana token balances object isLoadingEVM, // EVM loading state isLoadingSolana // Solana loading state } = useTokenBalances({ userEVMAddress: string, userSolanaAddress: string, tokenIn?: Token, // Optional: specific input token tokenOut?: Token // Optional: specific output token }); ``` ### useShogunBalances Direct access to the balance client for custom queries. ```typescript const balancesClient = useShogunBalances(); // Use client methods directly const evmBalance = await balancesClient.getEvmWalletBalance(address); const tokenPrice = await balancesClient.getTokenUSDPrice(tokenAddress, chainId); ``` ## Core Types ```typescript interface Token { address: string; // Token contract address decimals: number; // Token decimals (e.g., 18 for ETH) chainId: number; // Chain ID (e.g., 1 for Ethereum) symbol?: string; // Token symbol (optional) } interface SwapConfig { tokenIn: Token; // Source token tokenOut: Token; // Destination token setLatestSuggestedAutoSlippageValue: (value: number) => void; inputAmount: string; setInputAmount: (amount: string) => void; recipientAddress: string; slippage: number; dynamicSlippage?: boolean; // Optional: Enable dynamic slippage for Solana } interface SystemConfig { api: { key: string; url: string; }; systemFeePercent: number; userEVMAddress: string; userSolanaAddress: string; affiliateWallets: { solana: string; evm: string; }; notifyAboutError?: (error: Error) => void; // Optional: Error notification callback } interface QuoteContextValue { quotes: QuoteTypes | undefined; errors: { feeValidationError: string | null; balanceError: string | null; }; quoteRefetch: () => any; fees: ICollectedFees | undefined; isMaxBtnClicked: boolean; handleMaxBalanceInput: (quote: QuoteTypes | undefined, fees: ICollectedFees | undefined, maxBtnClicked: boolean) => void; setIsMaxBtnClicked: (isMaxBtnClicked: boolean) => void; inputValue: string; setInputValue: (inputValue: string) => void; needRecalculateMaxValue: boolean; isLoading: boolean; isRefetching: boolean; userInputAddress: string; userOutputAddress: string; } ``` ## Examples ### Basic Usage ```typescript import { useShogunQuote } from '@shogun-sdk/one-shot'; function SwapComponent() { const { quotes, isLoading, errors } = useShogunQuote(); if (isLoading) { return <div>Loading quote...</div>; } if (errors.balanceError) { return <div>Error: {errors.balanceError}</div>; } return ( <div> <h2>Swap Details</h2> <pre>{JSON.stringify(quotes, null, 2)}</pre> </div> ); } ``` ### Complete Example Here's a complete example of a swap interface: ```typescript import { useShogunQuote, useShogunBalances } from '@shogun-sdk/one-shot'; import { useState } from 'react'; function SwapInterface() { const [amount, setAmount] = useState('1000000000000000000'); // 1 ETH // Get quote const { quotes, isLoading: quoteLoading, errors } = useShogunQuote(); // Get balances const balancesClient = useShogunBalances(); if (quoteLoading) { return <div>Loading...</div>; } if (errors.balanceError) { return <div>Error fetching balances: {errors.balanceError}</div>; } if (errors.feeValidationError) { return <div>Error with fees: {errors.feeValidationError}</div>; } return ( <div className="swap-interface"> <h2>Cross-chain Swap</h2> {/* Amount Input */} <div className="input-group"> <label>Amount (ETH)</label> <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} min="0" step="0.000000000000000001" /> </div> {/* Swap Button */} <button onClick={() => {/* Handle swap */}} disabled={!quotes || quoteLoading} > {quoteLoading ? 'Loading...' : 'Swap'} </button> </div> ); } ``` ## Best Practices ### 1. Provider Setup - Always wrap your app with both providers in the correct order - Use environment variables for sensitive data - Memoize configuration objects to prevent unnecessary re-renders ```typescript // ✅ Good const api = useMemo(() => ({ key: process.env.SHOGUN_API_KEY!, url: process.env.SHOGUN_API_URL! }), []); // ❌ Bad - creates new object on every render const api = { key: process.env.SHOGUN_API_KEY!, url: process.env.SHOGUN_API_URL! }; ``` ### 2. Error Handling - Implement proper error boundaries - Use the provided error states from hooks - Implement the `notifyAboutError` callback for centralized error tracking ```typescript // ✅ Good error handling const { quotes, errors, isLoading } = useShogunQuote(); if (errors.balanceError) { return <div className="error">Insufficient balance: {errors.balanceError}</div>; } if (errors.feeValidationError) { return <div className="error">Fee validation failed: {errors.feeValidationError}</div>; } ``` ### 3. Performance Optimization - Use `useMemo` for expensive calculations - Implement proper loading states to improve user experience - Use the provided refetch functions when needed instead of forcing re-renders ```typescript // ✅ Good performance const expensiveCalculation = useMemo(() => { return calculateComplexValue(quotes, fees); }, [quotes, fees]); // ✅ Proper loading states if (isLoading) return <LoadingSpinner />; if (isRefetching) return <div>Updating quote...</div>; ``` ### 4. Type Safety - Use TypeScript for better type checking - Properly type your token configurations - Use the provided interfaces for better type safety ```typescript // ✅ Good typing const tokenIn: Token = { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', decimals: 18, chainId: 1, symbol: 'WETH' }; ``` ### 5. State Management - Keep provider state minimal - Use local component state for UI-specific data - Implement proper cleanup in useEffect hooks ```typescript // ✅ Good state management useEffect(() => { const timer = setInterval(refetchQuote, 30000); // Refetch every 30s return () => clearInterval(timer); // Cleanup }, [refetchQuote]); ``` ## Troubleshooting ### Common Issues and Solutions #### 1. Provider Issues ```typescript // ❌ Problem: Providers not in correct order <ShogunQuoteProvider> <ShogunBalancesProvider> <App /> </ShogunBalancesProvider> </ShogunQuoteProvider> // ✅ Solution: Correct provider order <ShogunBalancesProvider apiKey="..."> <ShogunQuoteProvider swap={...} system={...}> <App /> </ShogunQuoteProvider> </ShogunBalancesProvider> ``` #### 2. Hook Usage Issues ```typescript // ❌ Problem: Using hooks outside provider context function Component() { const { quotes } = useShogunQuote(); // Error: Hook used outside provider } // ✅ Solution: Ensure component is wrapped with providers <ShogunBalancesProvider> <ShogunQuoteProvider> <Component /> {/* Now hooks work properly */} </ShogunQuoteProvider> </ShogunBalancesProvider> ``` #### 3. Environment Variables ```typescript // ✅ Check environment variables are properly set if (!process.env.SHOGUN_API_KEY) { throw new Error('SHOGUN_API_KEY environment variable is required'); } if (!process.env.CODEX_API_KEY) { throw new Error('CODEX_API_KEY environment variable is required'); } ``` #### 4. Balance Loading Issues ```typescript // ✅ Handle loading states properly const { evmBalances, isLoadingEVM, isLoadingSolana } = useTokenBalances({ userEVMAddress: address || '', userSolanaAddress: solanaAddress || '', }); // Show loading state if (isLoadingEVM || isLoadingSolana) { return <div>Loading balances...</div>; } // Handle empty balances if (!evmBalances || Object.keys(evmBalances).length === 0) { return <div>No balances found</div>; } ``` #### 5. Quote Issues ```typescript // ✅ Debug quote problems const { quotes, errors, isLoading, quoteRefetch } = useShogunQuote(); // Check for specific errors if (errors.feeValidationError) { console.error('Fee validation error:', errors.feeValidationError); } if (errors.balanceError) { console.error('Balance error:', errors.balanceError); } // Manual refetch if needed const handleRetry = () => { quoteRefetch(); }; ``` ## Support - [GitHub Issues](https://github.com/shogun-network/shogun-sdk/issues) - [Main SDK Documentation](../../README.md) - [Discord Community](https://discord.gg/gundotfun) ## License ISC