227 lines
5.9 KiB
TypeScript
227 lines
5.9 KiB
TypeScript
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<void>;
|
|
setSelectedTier: (tier: FeeRecommendation['tier']) => void;
|
|
setAutoRefresh: (enabled: boolean) => void;
|
|
calculateFee: (txSizeBytes: number) => Promise<number>;
|
|
}
|
|
|
|
// 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<FeeAnalyticsState>()((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<any>('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<number>('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`;
|
|
}
|