import { create } from 'zustand'; import { invoke } from '@tauri-apps/api/core'; /** * Mempool statistics from the network */ export interface MempoolStats { txCount: number; totalSizeBytes: number; totalFees: number; minFeeRate: number; // sompi per byte avgFeeRate: number; maxFeeRate: number; percentile10: number; // fee rate at 10th percentile percentile50: number; // fee rate at 50th percentile (median) percentile90: number; // fee rate at 90th percentile lastUpdated: number; // unix timestamp } /** * Fee tier recommendation */ export interface FeeRecommendation { tier: 'economy' | 'standard' | 'priority' | 'instant'; feeRate: number; // sompi per byte estimatedBlocks: number; estimatedTimeSecs: number; description: string; } /** * Historical fee data point for charting */ export interface FeeHistoryPoint { timestamp: number; avgFeeRate: number; minFeeRate: number; maxFeeRate: number; blockHeight: number; } /** * Full fee analytics data */ export interface FeeAnalytics { mempool: MempoolStats; recommendations: FeeRecommendation[]; feeHistory: FeeHistoryPoint[]; networkCongestion: 'low' | 'medium' | 'high'; blockTargetTimeSecs: number; } interface FeeAnalyticsState { // State analytics: FeeAnalytics | null; selectedTier: FeeRecommendation['tier']; isLoading: boolean; error: string | null; autoRefresh: boolean; refreshIntervalMs: number; // Actions fetchAnalytics: () => Promise; setSelectedTier: (tier: FeeRecommendation['tier']) => void; setAutoRefresh: (enabled: boolean) => void; calculateFee: (txSizeBytes: number) => Promise; } // Transform snake_case backend response to camelCase function transformMempoolStats(data: any): MempoolStats { return { txCount: data.tx_count, totalSizeBytes: data.total_size_bytes, totalFees: data.total_fees, minFeeRate: data.min_fee_rate, avgFeeRate: data.avg_fee_rate, maxFeeRate: data.max_fee_rate, percentile10: data.percentile_10, percentile50: data.percentile_50, percentile90: data.percentile_90, lastUpdated: data.last_updated, }; } function transformRecommendation(data: any): FeeRecommendation { return { tier: data.tier as FeeRecommendation['tier'], feeRate: data.fee_rate, estimatedBlocks: data.estimated_blocks, estimatedTimeSecs: data.estimated_time_secs, description: data.description, }; } function transformFeeHistory(data: any): FeeHistoryPoint { return { timestamp: data.timestamp, avgFeeRate: data.avg_fee_rate, minFeeRate: data.min_fee_rate, maxFeeRate: data.max_fee_rate, blockHeight: data.block_height, }; } function transformAnalytics(data: any): FeeAnalytics { return { mempool: transformMempoolStats(data.mempool), recommendations: data.recommendations.map(transformRecommendation), feeHistory: data.fee_history.map(transformFeeHistory), networkCongestion: data.network_congestion as FeeAnalytics['networkCongestion'], blockTargetTimeSecs: data.block_target_time_secs, }; } export const useFeeAnalyticsStore = create()((set, get) => ({ // Initial state analytics: null, selectedTier: 'standard', isLoading: false, error: null, autoRefresh: true, refreshIntervalMs: 30000, // 30 seconds // Fetch all analytics data fetchAnalytics: async () => { set({ isLoading: true, error: null }); try { const data = await invoke('fee_get_analytics'); const analytics = transformAnalytics(data); set({ analytics, isLoading: false }); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to fetch fee analytics'; set({ error: message, isLoading: false }); } }, // Set selected fee tier setSelectedTier: (tier) => { set({ selectedTier: tier }); }, // Toggle auto-refresh setAutoRefresh: (enabled) => { set({ autoRefresh: enabled }); }, // Calculate fee for a transaction calculateFee: async (txSizeBytes) => { const { selectedTier } = get(); try { const fee = await invoke('fee_calculate', { txSizeBytes, tier: selectedTier, }); return fee; } catch (error) { // Fallback calculation const { analytics } = get(); const recommendation = analytics?.recommendations.find((r) => r.tier === selectedTier); const feeRate = recommendation?.feeRate || 1.0; return Math.ceil(txSizeBytes * feeRate); } }, })); /** * Get the selected recommendation */ export function useSelectedRecommendation(): FeeRecommendation | null { const { analytics, selectedTier } = useFeeAnalyticsStore(); return analytics?.recommendations.find((r) => r.tier === selectedTier) || null; } /** * Get congestion color class */ export function getCongestionColor(congestion: FeeAnalytics['networkCongestion']): string { switch (congestion) { case 'low': return 'text-green-400'; case 'medium': return 'text-yellow-400'; case 'high': return 'text-red-400'; default: return 'text-gray-400'; } } /** * Get congestion background color */ export function getCongestionBgColor(congestion: FeeAnalytics['networkCongestion']): string { switch (congestion) { case 'low': return 'bg-green-500/20 border-green-500/30'; case 'medium': return 'bg-yellow-500/20 border-yellow-500/30'; case 'high': return 'bg-red-500/20 border-red-500/30'; default: return 'bg-gray-500/20 border-gray-500/30'; } } /** * Format time duration in human readable format */ export function formatDuration(seconds: number): string { if (seconds < 60) { return `~${seconds}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); return `~${mins}m`; } else { const hours = Math.floor(seconds / 3600); return `~${hours}h`; } } /** * Format fee rate */ export function formatFeeRate(rate: number): string { return `${rate.toFixed(2)} sompi/byte`; }