Best Practices


description: Security and UX best practices for DApp developers

Best Practices

🛡️ Security Best Practices

1. Always Validate Wallet Installation

javascript
// ❌ Bad: Assuming wallet exists const address = await window.xrpl.connect(); // Throws if not installed // ✅ Good: Check before using function ensureWalletAvailable() { if (!window.xrpl || !window.xrpl.isDropFi) { throw new Error('DropFi wallet not installed'); } return true; } async function safeConnect() { try { ensureWalletAvailable(); const address = await window.xrpl.connect(); return address; } catch (error) { handleWalletError(error); } }

2. Validate All User Input

javascript
// ❌ Bad: Direct use of user input async function sendPayment(userAmount, userDestination) { await window.xrpl.sendTransaction({ TransactionType: 'Payment', Amount: userAmount * 1000000, Destination: userDestination }); } // ✅ Good: Validate and sanitize async function sendPaymentSafe(userAmount, userDestination) { // Validate destination if (!isValidXRPLAddress(userDestination)) { throw new Error('Invalid destination address'); } // Validate amount const amount = parseFloat(userAmount); if (isNaN(amount) || amount <= 0 || amount > 1000000) { throw new Error('Invalid amount'); } // Convert safely const drops = Math.floor(amount * 1000000).toString(); await window.xrpl.sendTransaction({ TransactionType: 'Payment', Account: window.xrpl.selectedAddress, Amount: drops, Destination: userDestination }); } function isValidXRPLAddress(address) { return /^r[a-zA-Z0-9]{24,33}$/.test(address); }

3. Handle Errors Gracefully

javascript
// ❌ Bad: Generic error handling try { await window.xrpl.connect(); } catch (error) { alert('Error: ' + error); } // ✅ Good: Specific error handling async function handleConnection() { try { const address = await window.xrpl.connect(); return { success: true, address }; } catch (error) { // User-friendly error messages const errorMessages = { 4001: 'You cancelled the connection request', 4100: 'Please unlock your wallet first', 4900: 'Wallet disconnected unexpectedly', NOT_INSTALLED: 'Please install DropFi wallet' }; const message = errorMessages[error.code] || 'Connection failed'; return { success: false, error: message, code: error.code }; } }

4. Implement Permission Checks

javascript
class WalletPermissions { constructor() { this.permissions = new Map(); } async checkPermission(action) { const address = window.xrpl.selectedAddress; if (!address) return false; const key = `${address}:${action}`; // Check cached permission if (this.permissions.has(key)) { return this.permissions.get(key); } // Request permission const granted = await this.requestPermission(action); this.permissions.set(key, granted); return granted; } async requestPermission(action) { // Show custom permission dialog return await showPermissionDialog({ title: 'Permission Required', message: `This app wants to ${action}`, address: window.xrpl.selectedAddress }); } revokePermissions(address) { // Clear permissions when disconnecting for (const [key] of this.permissions) { if (key.startsWith(address)) { this.permissions.delete(key); } } } }

5. Secure State Management

javascript
// ❌ Bad: Storing sensitive data in localStorage localStorage.setItem('userPrivateKey', privateKey); // NEVER DO THIS // ✅ Good: Only store non-sensitive data class SecureStorage { // Only store public data saveConnectionState(address, network) { const state = { address, network, connectedAt: Date.now(), // Never store: private keys, signatures, etc. }; sessionStorage.setItem('walletState', JSON.stringify(state)); } getConnectionState() { try { const state = JSON.parse(sessionStorage.getItem('walletState')); // Validate stored state is still valid if (state && Date.now() - state.connectedAt < 86400000) { // 24 hours return state; } // Clear expired state sessionStorage.removeItem('walletState'); return null; } catch { return null; } } clearState() { sessionStorage.removeItem('walletState'); } }

🎨 UX Best Practices

1. Provide Clear Connection Status

javascript
// React Component Example function WalletStatus() { const [status, setStatus] = useState('disconnected'); const [address, setAddress] = useState(null); useEffect(() => { const checkStatus = async () => { if (!window.xrpl) { setStatus('not-installed'); return; } const state = await window.xrpl.initialize(); if (state.selectedAddress) { setStatus('connected'); setAddress(state.selectedAddress); } else { setStatus('disconnected'); } }; checkStatus(); // Listen for changes const handleConnect = (addr) => { setStatus('connected'); setAddress(addr); }; const handleDisconnect = () => { setStatus('disconnected'); setAddress(null); }; window.xrpl?.on('xrpl_selectedAddress', handleConnect); window.xrpl?.on('xrpl_disconnect', handleDisconnect); return () => { window.xrpl?.off('xrpl_selectedAddress', handleConnect); window.xrpl?.off('xrpl_disconnect', handleDisconnect); }; }, []); const statusConfig = { 'not-installed': { icon: '🔴', text: 'Wallet Not Installed', action: () => window.open('https://dropzero.io') }, 'disconnected': { icon: '⚪', text: 'Connect Wallet', action: () => window.xrpl.connect() }, 'connected': { icon: '🟢', text: `${address?.slice(0, 6)}...${address?.slice(-4)}`, action: () => showAccountMenu() } }; const config = statusConfig[status]; return ( <button onClick={config.action} className={`wallet-status ${status}`}> <span className="status-icon">{config.icon}</span> <span className="status-text">{config.text}</span> </button> ); }

2. Loading States and Feedback

javascript
// Transaction with loading states function TransactionButton({ onSend }) { const [state, setState] = useState('idle'); // idle, signing, pending, success, error const [txHash, setTxHash] = useState(null); const handleTransaction = async () => { setState('signing'); try { const tx = await buildTransaction(); setState('pending'); const result = await window.xrpl.sendTransaction(tx); setState('success'); setTxHash(result.hash); // Reset after delay setTimeout(() => setState('idle'), 5000); } catch (error) { setState('error'); if (error.code !== 4001) { // Not user cancellation showErrorNotification(error.message); } setTimeout(() => setState('idle'), 3000); } }; const buttonStates = { idle: { text: 'Send Transaction', disabled: false }, signing: { text: 'Waiting for signature...', disabled: true }, pending: { text: 'Broadcasting...', disabled: true }, success: { text: '✓ Sent!', disabled: true }, error: { text: '✗ Failed', disabled: true } }; const { text, disabled } = buttonStates[state]; return ( <div className="transaction-button-wrapper"> <button onClick={handleTransaction} disabled={disabled} className={`tx-button ${state}`} > {state === 'signing' && <Spinner />} {text} </button> {txHash && ( <a href={`https://livenet.xrpl.org/transactions/${txHash}`} target="_blank" rel="noopener noreferrer" className="tx-link" > View on Explorer → </a> )} </div> ); }

3. Network Awareness

javascript
class NetworkManager { constructor() { this.network = null; this.listeners = new Set(); } async initialize() { if (!window.xrpl) return; // Get initial network const state = await window.xrpl.initialize(); this.setNetwork(state.network); // Listen for changes window.xrpl.on('xrpl_selectedNetwork', (network) => { this.setNetwork(network); }); } setNetwork(network) { const oldNetwork = this.network; this.network = network; if (oldNetwork !== network) { this.notifyListeners(network, oldNetwork); } } notifyListeners(newNetwork, oldNetwork) { this.listeners.forEach(listener => { listener(newNetwork, oldNetwork); }); } onNetworkChange(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } getNetworkConfig() { const configs = { mainnet: { name: 'Mainnet', color: '#23C48E', explorer: 'https://livenet.xrpl.org', warning: null }, testnet: { name: 'Testnet', color: '#FFA500', explorer: 'https://testnet.xrpl.org', warning: 'Test network - tokens have no value' }, devnet: { name: 'Devnet', color: '#FF6B6B', explorer: 'https://devnet.xrpl.org', warning: 'Development network - may reset' } }; return configs[this.network] || configs.mainnet; } } // Network indicator component function NetworkIndicator() { const [config, setConfig] = useState(null); const networkManager = useRef(new NetworkManager()); useEffect(() => { networkManager.current.initialize(); const unsubscribe = networkManager.current.onNetworkChange(() => { setConfig(networkManager.current.getNetworkConfig()); }); return unsubscribe; }, []); if (!config) return null; return ( <div className="network-indicator" style={{ backgroundColor: config.color }} > <span className="network-name">{config.name}</span> {config.warning && ( <span className="network-warning" title={config.warning}>⚠️</span> )} </div> ); }

4. Mobile Responsiveness

javascript
// Responsive wallet connection class ResponsiveWallet { constructor() { this.isMobile = this.checkMobile(); } checkMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); } async connect() { if (!window.xrpl) { if (this.isMobile) { // Mobile: Show QR code or deep link this.showMobileConnect(); } else { // Desktop: Show install prompt this.showInstallPrompt(); } return; } return window.xrpl.connect(); } showMobileConnect() { // Show modal with options const modal = document.createElement('div'); modal.className = 'wallet-connect-modal'; modal.innerHTML = ` <div class="modal-content"> <h3>Connect Wallet</h3> <p>Open DropFi app and scan this QR code:</p> <div id="qr-code"></div> <button onclick="window.open('dropzero://connect')"> Open in App </button> </div> `; document.body.appendChild(modal); } showInstallPrompt() { const modal = document.createElement('div'); modal.className = 'wallet-install-modal'; modal.innerHTML = ` <div class="modal-content"> <h3>DropFi Required</h3> <p>To use this DApp, please install the DropFi wallet extension.</p> <a href="https://dropzero.io/download" target="_blank" class="install-button"> Install DropFi </a> </div> `; document.body.appendChild(modal); } }

🚀 Performance Optimization

1. Debounce Event Handlers

javascript
// ❌ Bad: Direct event handling window.xrpl.on('xrpl_selectedAddress', (address) => { // This might fire rapidly updateUI(address); fetchBalance(address); loadTransactions(address); }); // ✅ Good: Debounced handling function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const handleAddressChange = debounce((address) => { updateUI(address); fetchBalance(address); loadTransactions(address); }, 300); window.xrpl.on('xrpl_selectedAddress', handleAddressChange);

2. Cache Wallet State

javascript
class WalletStateCache { constructor() { this.cache = new Map(); this.ttl = 5000; // 5 seconds } async getState() { const cached = this.cache.get('state'); if (cached && Date.now() - cached.timestamp < this.ttl) { return cached.data; } // Fetch fresh state const state = await window.xrpl.initialize(); this.cache.set('state', { data: state, timestamp: Date.now() }); return state; } invalidate() { this.cache.clear(); } } // Use throughout app const walletCache = new WalletStateCache(); // Invalidate on changes window.xrpl.on('xrpl_selectedAddress', () => walletCache.invalidate()); window.xrpl.on('xrpl_selectedNetwork', () => walletCache.invalidate());

3. Lazy Load Wallet Features

javascript
// Only load wallet features when needed class LazyWalletLoader { constructor() { this.initialized = false; this.initPromise = null; } async ensureInitialized() { if (this.initialized) return true; if (this.initPromise) return this.initPromise; this.initPromise = this.initialize(); const result = await this.initPromise; this.initialized = result; return result; } async initialize() { // Check if wallet available if (!window.xrpl) { // Try to wait for injection await new Promise(resolve => { let attempts = 0; const check = setInterval(() => { attempts++; if (window.xrpl || attempts > 10) { clearInterval(check); resolve(); } }, 100); }); } if (!window.xrpl) return false; // Setup listeners only when wallet detected this.setupEventListeners(); return true; } setupEventListeners() { // Lazy load event handlers import('./walletEventHandlers').then(module => { module.setupHandlers(); }); } }

📱 Framework Integration

React Best Practices

javascript
// Custom hook with all best practices function useDropZero() { const [state, setState] = useState({ isInstalled: false, isConnected: false, address: null, network: null, loading: true, error: null }); const stateRef = useRef(state); stateRef.current = state; useEffect(() => { let mounted = true; const init = async () => { // Check installation if (!window.xrpl) { if (mounted) { setState(prev => ({ ...prev, isInstalled: false, loading: false })); } return; } try { // Get initial state const walletState = await window.xrpl.initialize(); if (mounted) { setState({ isInstalled: true, isConnected: !!walletState.selectedAddress, address: walletState.selectedAddress, network: walletState.network, loading: false, error: null }); } } catch (error) { if (mounted) { setState(prev => ({ ...prev, loading: false, error: error.message })); } } }; init(); // Cleanup return () => { mounted = false; }; }, []); // Event listeners useEffect(() => { if (!window.xrpl) return; const handlers = { xrpl_selectedAddress: (address) => { setState(prev => ({ ...prev, address, isConnected: !!address })); }, xrpl_selectedNetwork: (network) => { setState(prev => ({ ...prev, network })); }, xrpl_disconnect: () => { setState(prev => ({ ...prev, address: null, isConnected: false })); } }; // Subscribe to events Object.entries(handlers).forEach(([event, handler]) => { window.xrpl.on(event, handler); }); // Cleanup return () => { Object.entries(handlers).forEach(([event, handler]) => { window.xrpl.off(event, handler); }); }; }, [state.isInstalled]); // Methods const connect = useCallback(async () => { if (!state.isInstalled) { throw new Error('DropFi not installed'); } try { const address = await window.xrpl.connect(); return address; } catch (error) { setState(prev => ({ ...prev, error: error.message })); throw error; } }, [state.isInstalled]); const disconnect = useCallback(async () => { if (!state.address) return; try { await window.xrpl.disconnect(state.address); } catch (error) { console.error('Disconnect error:', error); } }, [state.address]); const sendTransaction = useCallback(async (tx) => { if (!state.isConnected) { throw new Error('Wallet not connected'); } const transaction = { ...tx, Account: tx.Account || state.address }; return window.xrpl.sendTransaction(transaction); }, [state.isConnected, state.address]); return { ...state, connect, disconnect, sendTransaction }; }

🎯 Key Takeaway: Focus on security, user experience, and performance. Always validate inputs, handle errors gracefully, and provide clear feedback to users throughout their wallet interactions.