keynesol-shared 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -0
- package/dist/components/Common/ErrorBoundary.d.ts +23 -0
- package/dist/components/Common/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/Common/ErrorBoundary.js +93 -0
- package/dist/components/Common/ErrorBoundary.jsx +103 -0
- package/dist/components/Common/ErrorMessage.d.ts +8 -0
- package/dist/components/Common/ErrorMessage.d.ts.map +1 -0
- package/dist/components/Common/ErrorMessage.js +36 -0
- package/dist/components/Common/ErrorMessage.jsx +40 -0
- package/dist/components/Common/Loading.d.ts +8 -0
- package/dist/components/Common/Loading.d.ts.map +1 -0
- package/dist/components/Common/Loading.js +41 -0
- package/dist/components/Common/Loading.jsx +44 -0
- package/dist/components/Common/LoadingIndicator.d.ts +17 -0
- package/dist/components/Common/LoadingIndicator.d.ts.map +1 -0
- package/dist/components/Common/LoadingIndicator.js +95 -0
- package/dist/components/Common/LoadingIndicator.jsx +108 -0
- package/dist/components/Common/ProgramStatus.d.ts +3 -0
- package/dist/components/Common/ProgramStatus.d.ts.map +1 -0
- package/dist/components/Common/ProgramStatus.js +26 -0
- package/dist/components/Common/ProgramStatus.jsx +27 -0
- package/dist/components/Common/Skeleton.d.ts +39 -0
- package/dist/components/Common/Skeleton.d.ts.map +1 -0
- package/dist/components/Common/Skeleton.js +53 -0
- package/dist/components/Common/Skeleton.jsx +67 -0
- package/dist/components/Common/SkeletonScreen.d.ts +18 -0
- package/dist/components/Common/SkeletonScreen.d.ts.map +1 -0
- package/dist/components/Common/SkeletonScreen.js +98 -0
- package/dist/components/Common/SkeletonScreen.jsx +108 -0
- package/dist/components/Common/index.d.ts +11 -0
- package/dist/components/Common/index.d.ts.map +1 -0
- package/dist/components/Common/index.js +10 -0
- package/dist/components/Wallet/TransactionStatus.d.ts +11 -0
- package/dist/components/Wallet/TransactionStatus.d.ts.map +1 -0
- package/dist/components/Wallet/TransactionStatus.js +97 -0
- package/dist/components/Wallet/TransactionStatus.jsx +106 -0
- package/dist/components/Wallet/WalletBalance.d.ts +4 -0
- package/dist/components/Wallet/WalletBalance.d.ts.map +1 -0
- package/dist/components/Wallet/WalletBalance.js +82 -0
- package/dist/components/Wallet/WalletBalance.jsx +86 -0
- package/dist/components/Wallet/WalletButton.d.ts +3 -0
- package/dist/components/Wallet/WalletButton.d.ts.map +1 -0
- package/dist/components/Wallet/WalletButton.js +51 -0
- package/dist/components/Wallet/WalletButton.jsx +53 -0
- package/dist/components/Wallet/WalletConnectionModal.d.ts +8 -0
- package/dist/components/Wallet/WalletConnectionModal.d.ts.map +1 -0
- package/dist/components/Wallet/WalletConnectionModal.js +150 -0
- package/dist/components/Wallet/WalletConnectionModal.jsx +170 -0
- package/dist/components/Wallet/WalletProvider.d.ts +9 -0
- package/dist/components/Wallet/WalletProvider.d.ts.map +1 -0
- package/dist/components/Wallet/WalletProvider.js +70 -0
- package/dist/components/Wallet/WalletProvider.jsx +75 -0
- package/dist/components/Wallet/index.d.ts +9 -0
- package/dist/components/Wallet/index.d.ts.map +1 -0
- package/dist/components/Wallet/index.js +8 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +6 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useCache.d.ts +16 -0
- package/dist/hooks/useCache.d.ts.map +1 -0
- package/dist/hooks/useCache.js +67 -0
- package/dist/hooks/usePolling.d.ts +16 -0
- package/dist/hooks/usePolling.d.ts.map +1 -0
- package/dist/hooks/usePolling.js +79 -0
- package/dist/hooks/useProgram.d.ts +14 -0
- package/dist/hooks/useProgram.d.ts.map +1 -0
- package/dist/hooks/useProgram.js +88 -0
- package/dist/hooks/useTokenBalance.d.ts +16 -0
- package/dist/hooks/useTokenBalance.d.ts.map +1 -0
- package/dist/hooks/useTokenBalance.js +100 -0
- package/dist/hooks/useVaults.d.ts +23 -0
- package/dist/hooks/useVaults.d.ts.map +1 -0
- package/dist/hooks/useVaults.js +98 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +6 -0
- package/dist/services/reconciliationService.d.ts +76 -0
- package/dist/services/reconciliationService.d.ts.map +1 -0
- package/dist/services/reconciliationService.js +216 -0
- package/dist/services/syncService.d.ts +51 -0
- package/dist/services/syncService.d.ts.map +1 -0
- package/dist/services/syncService.js +218 -0
- package/dist/types/index.d.ts +201 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/cacheManager.d.ts +73 -0
- package/dist/utils/cacheManager.d.ts.map +1 -0
- package/dist/utils/cacheManager.js +232 -0
- package/dist/utils/errorHandler.d.ts +76 -0
- package/dist/utils/errorHandler.d.ts.map +1 -0
- package/dist/utils/errorHandler.js +267 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/performanceMonitor.d.ts +75 -0
- package/dist/utils/performanceMonitor.d.ts.map +1 -0
- package/dist/utils/performanceMonitor.js +197 -0
- package/dist/utils/rpcRetry.d.ts +12 -0
- package/dist/utils/rpcRetry.d.ts.map +1 -0
- package/dist/utils/rpcRetry.js +47 -0
- package/dist/utils/supabase.d.ts +198 -0
- package/dist/utils/supabase.d.ts.map +1 -0
- package/dist/utils/supabase.js +50 -0
- package/dist/utils/toastService.d.ts +52 -0
- package/dist/utils/toastService.d.ts.map +1 -0
- package/dist/utils/toastService.js +139 -0
- package/dist/utils/tokenUtils.d.ts +33 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -0
- package/dist/utils/tokenUtils.js +66 -0
- package/dist/utils/validation.d.ts +35 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +83 -0
- package/package.json +45 -0
- package/src/components/Common/ErrorBoundary.tsx +135 -0
- package/src/components/Common/ErrorMessage.tsx +52 -0
- package/src/components/Common/Loading.tsx +56 -0
- package/src/components/Common/LoadingIndicator.tsx +143 -0
- package/src/components/Common/ProgramStatus.tsx +37 -0
- package/src/components/Common/Skeleton.tsx +83 -0
- package/src/components/Common/SkeletonScreen.tsx +166 -0
- package/src/components/Common/index.ts +10 -0
- package/src/components/Wallet/TransactionStatus.tsx +138 -0
- package/src/components/Wallet/WalletBalance.tsx +94 -0
- package/src/components/Wallet/WalletButton.tsx +65 -0
- package/src/components/Wallet/WalletConnectionModal.tsx +193 -0
- package/src/components/Wallet/WalletProvider.tsx +104 -0
- package/src/components/Wallet/index.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCache.ts +87 -0
- package/src/hooks/usePolling.ts +98 -0
- package/src/hooks/useProgram.ts +93 -0
- package/src/hooks/useTokenBalance.ts +113 -0
- package/src/hooks/useVaults.ts +122 -0
- package/src/index.ts +23 -0
- package/src/services/index.ts +6 -0
- package/src/services/reconciliationService.ts +246 -0
- package/src/services/syncService.ts +238 -0
- package/src/types/index.ts +233 -0
- package/src/utils/cacheManager.ts +286 -0
- package/src/utils/errorHandler.ts +336 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/performanceMonitor.ts +222 -0
- package/src/utils/rpcRetry.ts +55 -0
- package/src/utils/supabase.ts +253 -0
- package/src/utils/toastService.ts +166 -0
- package/src/utils/tokenUtils.ts +75 -0
- package/src/utils/validation.ts +107 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager
|
|
3
|
+
* Multi-level caching system for blockchain data
|
|
4
|
+
* Requirements: 6.2, 6.3
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type CacheStrategy = 'lru' | 'lfu' | 'fifo';
|
|
8
|
+
|
|
9
|
+
export interface CacheConfig {
|
|
10
|
+
ttl: number; // milliseconds
|
|
11
|
+
maxSize?: number; // maximum cache entries
|
|
12
|
+
strategy: CacheStrategy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CacheEntry<T> {
|
|
16
|
+
data: T;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
hits: number;
|
|
19
|
+
ttl: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CacheStats {
|
|
23
|
+
hits: number;
|
|
24
|
+
misses: number;
|
|
25
|
+
hitRate: number;
|
|
26
|
+
size: number;
|
|
27
|
+
oldestEntry: number;
|
|
28
|
+
newestEntry: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class CacheManager {
|
|
32
|
+
private caches: Map<string, Map<string, CacheEntry<any>>> = new Map();
|
|
33
|
+
private stats: Map<string, { hits: number; misses: number }> = new Map();
|
|
34
|
+
private configs: Map<string, CacheConfig> = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get cached data
|
|
38
|
+
*/
|
|
39
|
+
get<T>(namespace: string, key: string): T | null {
|
|
40
|
+
const cache = this.caches.get(namespace);
|
|
41
|
+
if (!cache) {
|
|
42
|
+
this.recordMiss(namespace);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entry = cache.get(key);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
this.recordMiss(namespace);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if entry is expired
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
55
|
+
cache.delete(key);
|
|
56
|
+
this.recordMiss(namespace);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Update hit count
|
|
61
|
+
entry.hits++;
|
|
62
|
+
this.recordHit(namespace);
|
|
63
|
+
|
|
64
|
+
return entry.data as T;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set cached data
|
|
69
|
+
*/
|
|
70
|
+
set<T>(namespace: string, key: string, data: T, ttl?: number): void {
|
|
71
|
+
const config = this.configs.get(namespace);
|
|
72
|
+
if (!config) {
|
|
73
|
+
console.warn(`No config found for namespace: ${namespace}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let cache = this.caches.get(namespace);
|
|
78
|
+
if (!cache) {
|
|
79
|
+
cache = new Map();
|
|
80
|
+
this.caches.set(namespace, cache);
|
|
81
|
+
this.stats.set(namespace, { hits: 0, misses: 0 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const entryTtl = ttl || config.ttl;
|
|
85
|
+
const entry: CacheEntry<T> = {
|
|
86
|
+
data,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
hits: 0,
|
|
89
|
+
ttl: entryTtl,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Check max size and evict if needed
|
|
93
|
+
if (config.maxSize && cache.size >= config.maxSize) {
|
|
94
|
+
this.evict(namespace, cache, config.strategy);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cache.set(key, entry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Invalidate cache entry or entire namespace
|
|
102
|
+
*/
|
|
103
|
+
invalidate(namespace: string, key?: string): void {
|
|
104
|
+
const cache = this.caches.get(namespace);
|
|
105
|
+
if (!cache) return;
|
|
106
|
+
|
|
107
|
+
if (key) {
|
|
108
|
+
cache.delete(key);
|
|
109
|
+
} else {
|
|
110
|
+
cache.clear();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clear all caches or specific namespace
|
|
116
|
+
*/
|
|
117
|
+
clear(namespace?: string): void {
|
|
118
|
+
if (namespace) {
|
|
119
|
+
this.caches.delete(namespace);
|
|
120
|
+
this.stats.delete(namespace);
|
|
121
|
+
} else {
|
|
122
|
+
this.caches.clear();
|
|
123
|
+
this.stats.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Configure cache for namespace
|
|
129
|
+
*/
|
|
130
|
+
configure(namespace: string, config: CacheConfig): void {
|
|
131
|
+
this.configs.set(namespace, config);
|
|
132
|
+
if (!this.stats.has(namespace)) {
|
|
133
|
+
this.stats.set(namespace, { hits: 0, misses: 0 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get cache statistics
|
|
139
|
+
*/
|
|
140
|
+
getStats(namespace: string): CacheStats | null {
|
|
141
|
+
const cache = this.caches.get(namespace);
|
|
142
|
+
const stats = this.stats.get(namespace);
|
|
143
|
+
if (!cache || !stats) return null;
|
|
144
|
+
|
|
145
|
+
const entries = Array.from(cache.values());
|
|
146
|
+
const timestamps = entries.map(e => e.timestamp);
|
|
147
|
+
const totalRequests = stats.hits + stats.misses;
|
|
148
|
+
const hitRate = totalRequests > 0 ? stats.hits / totalRequests : 0;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
hits: stats.hits,
|
|
152
|
+
misses: stats.misses,
|
|
153
|
+
hitRate,
|
|
154
|
+
size: cache.size,
|
|
155
|
+
oldestEntry: timestamps.length > 0 ? Math.min(...timestamps) : 0,
|
|
156
|
+
newestEntry: timestamps.length > 0 ? Math.max(...timestamps) : 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Evict entry based on strategy
|
|
162
|
+
*/
|
|
163
|
+
private evict(
|
|
164
|
+
namespace: string,
|
|
165
|
+
cache: Map<string, CacheEntry<any>>,
|
|
166
|
+
strategy: CacheStrategy
|
|
167
|
+
): void {
|
|
168
|
+
let entryToEvict: string | null = null;
|
|
169
|
+
|
|
170
|
+
switch (strategy) {
|
|
171
|
+
case 'lru': {
|
|
172
|
+
// Evict least recently used (oldest timestamp)
|
|
173
|
+
let oldestTime = Infinity;
|
|
174
|
+
const entries = Array.from(cache.entries());
|
|
175
|
+
for (const [key, entry] of entries) {
|
|
176
|
+
if (entry.timestamp < oldestTime) {
|
|
177
|
+
oldestTime = entry.timestamp;
|
|
178
|
+
entryToEvict = key;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'lfu': {
|
|
185
|
+
// Evict least frequently used (lowest hits)
|
|
186
|
+
let lowestHits = Infinity;
|
|
187
|
+
const entries = Array.from(cache.entries());
|
|
188
|
+
for (const [key, entry] of entries) {
|
|
189
|
+
if (entry.hits < lowestHits) {
|
|
190
|
+
lowestHits = entry.hits;
|
|
191
|
+
entryToEvict = key;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'fifo': {
|
|
198
|
+
// Evict first in (oldest timestamp, same as LRU for simplicity)
|
|
199
|
+
let oldestTime = Infinity;
|
|
200
|
+
const entries = Array.from(cache.entries());
|
|
201
|
+
for (const [key, entry] of entries) {
|
|
202
|
+
if (entry.timestamp < oldestTime) {
|
|
203
|
+
oldestTime = entry.timestamp;
|
|
204
|
+
entryToEvict = key;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (entryToEvict) {
|
|
212
|
+
cache.delete(entryToEvict);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Record cache hit
|
|
218
|
+
*/
|
|
219
|
+
private recordHit(namespace: string): void {
|
|
220
|
+
const stats = this.stats.get(namespace);
|
|
221
|
+
if (stats) {
|
|
222
|
+
stats.hits++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Record cache miss
|
|
228
|
+
*/
|
|
229
|
+
private recordMiss(namespace: string): void {
|
|
230
|
+
const stats = this.stats.get(namespace);
|
|
231
|
+
if (stats) {
|
|
232
|
+
stats.misses++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clean expired entries (should be called periodically)
|
|
238
|
+
*/
|
|
239
|
+
cleanExpired(): void {
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const namespaces = Array.from(this.caches.entries());
|
|
242
|
+
for (const [namespace, cache] of namespaces) {
|
|
243
|
+
const entries = Array.from(cache.entries());
|
|
244
|
+
for (const [key, entry] of entries) {
|
|
245
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
246
|
+
cache.delete(key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Export singleton instance
|
|
254
|
+
export const cacheManager = new CacheManager();
|
|
255
|
+
|
|
256
|
+
// Configure default caches
|
|
257
|
+
cacheManager.configure('vaults', {
|
|
258
|
+
ttl: 30000, // 30 seconds
|
|
259
|
+
maxSize: 100,
|
|
260
|
+
strategy: 'lru',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
cacheManager.configure('userStakes', {
|
|
264
|
+
ttl: 10000, // 10 seconds
|
|
265
|
+
maxSize: 1000,
|
|
266
|
+
strategy: 'lru',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
cacheManager.configure('platformMetrics', {
|
|
270
|
+
ttl: 60000, // 1 minute
|
|
271
|
+
maxSize: 50,
|
|
272
|
+
strategy: 'lru',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
cacheManager.configure('transactions', {
|
|
276
|
+
ttl: 60000, // 1 minute
|
|
277
|
+
maxSize: 500,
|
|
278
|
+
strategy: 'lru',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Clean expired entries every 5 minutes
|
|
282
|
+
if (typeof window !== 'undefined') {
|
|
283
|
+
setInterval(() => {
|
|
284
|
+
cacheManager.cleanExpired();
|
|
285
|
+
}, 5 * 60 * 1000);
|
|
286
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Utility
|
|
3
|
+
* Comprehensive error handling system with classification, retry logic, and user-friendly messages
|
|
4
|
+
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export enum ErrorCategory {
|
|
8
|
+
NETWORK = 'network',
|
|
9
|
+
BLOCKCHAIN = 'blockchain',
|
|
10
|
+
VALIDATION = 'validation',
|
|
11
|
+
AUTHORIZATION = 'authorization',
|
|
12
|
+
DATA = 'data',
|
|
13
|
+
UNKNOWN = 'unknown',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ErrorHandlerConfig {
|
|
17
|
+
retryAttempts?: number;
|
|
18
|
+
retryDelay?: number; // milliseconds
|
|
19
|
+
exponentialBackoff?: boolean;
|
|
20
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ErrorLog {
|
|
24
|
+
timestamp: number;
|
|
25
|
+
level: 'error' | 'warning' | 'info';
|
|
26
|
+
context: string;
|
|
27
|
+
message: string;
|
|
28
|
+
stack?: string;
|
|
29
|
+
metadata?: Record<string, any>;
|
|
30
|
+
userId?: string;
|
|
31
|
+
category: ErrorCategory;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ErrorHandler {
|
|
35
|
+
private logs: ErrorLog[] = [];
|
|
36
|
+
private maxLogs: number = 1000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Classify error into category
|
|
40
|
+
*/
|
|
41
|
+
classifyError(error: Error): ErrorCategory {
|
|
42
|
+
const message = error.message.toLowerCase();
|
|
43
|
+
const stack = error.stack?.toLowerCase() || '';
|
|
44
|
+
|
|
45
|
+
// Network errors
|
|
46
|
+
if (
|
|
47
|
+
message.includes('network') ||
|
|
48
|
+
message.includes('fetch') ||
|
|
49
|
+
message.includes('timeout') ||
|
|
50
|
+
message.includes('connection') ||
|
|
51
|
+
message.includes('rpc') ||
|
|
52
|
+
message.includes('failed to fetch')
|
|
53
|
+
) {
|
|
54
|
+
return ErrorCategory.NETWORK;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Blockchain errors
|
|
58
|
+
if (
|
|
59
|
+
message.includes('transaction') ||
|
|
60
|
+
message.includes('insufficient') ||
|
|
61
|
+
message.includes('program') ||
|
|
62
|
+
message.includes('account') ||
|
|
63
|
+
message.includes('anchor') ||
|
|
64
|
+
message.includes('solana')
|
|
65
|
+
) {
|
|
66
|
+
return ErrorCategory.BLOCKCHAIN;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validation errors
|
|
70
|
+
if (
|
|
71
|
+
message.includes('invalid') ||
|
|
72
|
+
message.includes('required') ||
|
|
73
|
+
message.includes('must be') ||
|
|
74
|
+
message.includes('out of range') ||
|
|
75
|
+
message.includes('validation')
|
|
76
|
+
) {
|
|
77
|
+
return ErrorCategory.VALIDATION;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Authorization errors
|
|
81
|
+
if (
|
|
82
|
+
message.includes('unauthorized') ||
|
|
83
|
+
message.includes('permission') ||
|
|
84
|
+
message.includes('access denied') ||
|
|
85
|
+
message.includes('wallet not connected') ||
|
|
86
|
+
message.includes('signature')
|
|
87
|
+
) {
|
|
88
|
+
return ErrorCategory.AUTHORIZATION;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Data errors
|
|
92
|
+
if (
|
|
93
|
+
message.includes('parse') ||
|
|
94
|
+
message.includes('json') ||
|
|
95
|
+
message.includes('data') ||
|
|
96
|
+
message.includes('supabase') ||
|
|
97
|
+
message.includes('database')
|
|
98
|
+
) {
|
|
99
|
+
return ErrorCategory.DATA;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return ErrorCategory.UNKNOWN;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get user-friendly error message
|
|
107
|
+
*/
|
|
108
|
+
getErrorMessage(error: Error): string {
|
|
109
|
+
const category = this.classifyError(error);
|
|
110
|
+
const message = error.message;
|
|
111
|
+
|
|
112
|
+
switch (category) {
|
|
113
|
+
case ErrorCategory.NETWORK:
|
|
114
|
+
if (message.includes('timeout')) {
|
|
115
|
+
return 'Connection timeout. Please check your internet connection and try again.';
|
|
116
|
+
}
|
|
117
|
+
if (message.includes('rpc')) {
|
|
118
|
+
return 'Blockchain connection failed. Please try again in a moment.';
|
|
119
|
+
}
|
|
120
|
+
return 'Network error. Please check your connection and try again.';
|
|
121
|
+
|
|
122
|
+
case ErrorCategory.BLOCKCHAIN:
|
|
123
|
+
if (message.includes('insufficient')) {
|
|
124
|
+
return 'Insufficient balance. Please add more tokens to your wallet.';
|
|
125
|
+
}
|
|
126
|
+
if (message.includes('user rejected')) {
|
|
127
|
+
return 'Transaction cancelled.';
|
|
128
|
+
}
|
|
129
|
+
if (message.includes('slippage')) {
|
|
130
|
+
return 'Transaction failed due to price movement. Please try again.';
|
|
131
|
+
}
|
|
132
|
+
if (message.includes('simulation')) {
|
|
133
|
+
return 'Transaction simulation failed. Please check your inputs and try again.';
|
|
134
|
+
}
|
|
135
|
+
return 'Transaction failed. Please try again or contact support if the issue persists.';
|
|
136
|
+
|
|
137
|
+
case ErrorCategory.VALIDATION:
|
|
138
|
+
return message || 'Invalid input. Please check your values and try again.';
|
|
139
|
+
|
|
140
|
+
case ErrorCategory.AUTHORIZATION:
|
|
141
|
+
if (message.includes('wallet not connected')) {
|
|
142
|
+
return 'Please connect your wallet to continue.';
|
|
143
|
+
}
|
|
144
|
+
if (message.includes('unauthorized')) {
|
|
145
|
+
return 'You do not have permission to perform this action.';
|
|
146
|
+
}
|
|
147
|
+
return 'Authorization required. Please connect your wallet.';
|
|
148
|
+
|
|
149
|
+
case ErrorCategory.DATA:
|
|
150
|
+
return 'Data error. Please refresh the page and try again.';
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
return message || 'An unexpected error occurred. Please try again.';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get recovery suggestion based on error category
|
|
159
|
+
*/
|
|
160
|
+
getRecoverySuggestion(error: Error): string | null {
|
|
161
|
+
const category = this.classifyError(error);
|
|
162
|
+
|
|
163
|
+
switch (category) {
|
|
164
|
+
case ErrorCategory.NETWORK:
|
|
165
|
+
return 'Check your internet connection and try again. If the problem persists, try refreshing the page.';
|
|
166
|
+
|
|
167
|
+
case ErrorCategory.BLOCKCHAIN:
|
|
168
|
+
return 'Verify your wallet has sufficient balance and try again. If the issue continues, check the transaction on Solana Explorer.';
|
|
169
|
+
|
|
170
|
+
case ErrorCategory.VALIDATION:
|
|
171
|
+
return 'Please review your inputs and ensure all required fields are filled correctly.';
|
|
172
|
+
|
|
173
|
+
case ErrorCategory.AUTHORIZATION:
|
|
174
|
+
return 'Please connect your wallet and ensure you have the necessary permissions.';
|
|
175
|
+
|
|
176
|
+
case ErrorCategory.DATA:
|
|
177
|
+
return 'Try refreshing the page. If the problem persists, contact support.';
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Log error with context
|
|
186
|
+
*/
|
|
187
|
+
logError(
|
|
188
|
+
error: Error,
|
|
189
|
+
context: string,
|
|
190
|
+
metadata?: Record<string, any>
|
|
191
|
+
): void {
|
|
192
|
+
const category = this.classifyError(error);
|
|
193
|
+
const errorLog: ErrorLog = {
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
level: 'error',
|
|
196
|
+
context,
|
|
197
|
+
message: error.message,
|
|
198
|
+
stack: error.stack,
|
|
199
|
+
metadata,
|
|
200
|
+
userId: this.getCurrentUserId(),
|
|
201
|
+
category,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Add to in-memory log
|
|
205
|
+
this.logs.push(errorLog);
|
|
206
|
+
if (this.logs.length > this.maxLogs) {
|
|
207
|
+
this.logs.shift();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Log to console in development
|
|
211
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
|
|
212
|
+
console.error(`[${context}]`, error, metadata);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Send to monitoring service in production
|
|
216
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
|
|
217
|
+
this.sendToMonitoring(errorLog);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Wrap async function with error handling and retry logic
|
|
223
|
+
*/
|
|
224
|
+
async wrapAsync<T>(
|
|
225
|
+
fn: () => Promise<T>,
|
|
226
|
+
config?: ErrorHandlerConfig
|
|
227
|
+
): Promise<T> {
|
|
228
|
+
const {
|
|
229
|
+
retryAttempts = 3,
|
|
230
|
+
retryDelay = 1000,
|
|
231
|
+
exponentialBackoff = true,
|
|
232
|
+
onRetry,
|
|
233
|
+
} = config || {};
|
|
234
|
+
|
|
235
|
+
let lastError: Error;
|
|
236
|
+
let attempt = 0;
|
|
237
|
+
|
|
238
|
+
while (attempt < retryAttempts) {
|
|
239
|
+
try {
|
|
240
|
+
return await fn();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
lastError = error as Error;
|
|
243
|
+
attempt++;
|
|
244
|
+
|
|
245
|
+
// Don't retry validation errors
|
|
246
|
+
if (this.classifyError(lastError) === ErrorCategory.VALIDATION) {
|
|
247
|
+
throw lastError;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Don't retry on final attempt
|
|
251
|
+
if (attempt >= retryAttempts) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Calculate delay with exponential backoff
|
|
256
|
+
const delay = exponentialBackoff
|
|
257
|
+
? retryDelay * Math.pow(2, attempt - 1)
|
|
258
|
+
: retryDelay;
|
|
259
|
+
|
|
260
|
+
// Add jitter to prevent thundering herd
|
|
261
|
+
const jitter = Math.random() * 500;
|
|
262
|
+
|
|
263
|
+
// Call retry callback
|
|
264
|
+
if (onRetry) {
|
|
265
|
+
onRetry(attempt, lastError);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw lastError!;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle error with appropriate recovery strategy
|
|
277
|
+
*/
|
|
278
|
+
handleError(
|
|
279
|
+
error: Error,
|
|
280
|
+
context: string,
|
|
281
|
+
config?: ErrorHandlerConfig
|
|
282
|
+
): void {
|
|
283
|
+
this.logError(error, context);
|
|
284
|
+
|
|
285
|
+
const category = this.classifyError(error);
|
|
286
|
+
const message = this.getErrorMessage(error);
|
|
287
|
+
const suggestion = this.getRecoverySuggestion(error);
|
|
288
|
+
|
|
289
|
+
// In a real implementation, this would trigger toast notifications
|
|
290
|
+
// For now, we'll just log it
|
|
291
|
+
console.error(`[${context}] ${message}`, suggestion ? `Suggestion: ${suggestion}` : '');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get recent errors for debugging
|
|
296
|
+
*/
|
|
297
|
+
getRecentErrors(count: number = 10): ErrorLog[] {
|
|
298
|
+
return this.logs.slice(-count);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Clear error logs
|
|
303
|
+
*/
|
|
304
|
+
clearLogs(): void {
|
|
305
|
+
this.logs = [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get current user ID (if available)
|
|
310
|
+
*/
|
|
311
|
+
private getCurrentUserId(): string | undefined {
|
|
312
|
+
// In a real implementation, this would get the current user's public key
|
|
313
|
+
if (typeof window !== 'undefined') {
|
|
314
|
+
// Could get from wallet connection state
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Send error to monitoring service
|
|
322
|
+
*/
|
|
323
|
+
private async sendToMonitoring(errorLog: ErrorLog): Promise<void> {
|
|
324
|
+
// In production, send to monitoring service (e.g., Sentry, LogRocket)
|
|
325
|
+
// For now, we'll just store it locally
|
|
326
|
+
try {
|
|
327
|
+
// Could send to Supabase or external service
|
|
328
|
+
// await supabase.from('error_logs').insert(errorLog);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
console.error('Failed to send error to monitoring:', e);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Export singleton instance
|
|
336
|
+
export const errorHandler = new ErrorHandler();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Index
|
|
3
|
+
* Export all utility functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './errorHandler';
|
|
7
|
+
export * from './cacheManager';
|
|
8
|
+
export * from './tokenUtils';
|
|
9
|
+
export * from './validation';
|
|
10
|
+
export * from './toastService';
|
|
11
|
+
export * from './rpcRetry';
|
|
12
|
+
export * from './performanceMonitor';
|