@monygroupcorp/micro-web3 0.1.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 +87 -0
- package/dist/micro-web3.cjs.js +10 -0
- package/dist/micro-web3.cjs.js.map +1 -0
- package/dist/micro-web3.esm.js +10 -0
- package/dist/micro-web3.esm.js.map +1 -0
- package/dist/micro-web3.umd.js +10 -0
- package/dist/micro-web3.umd.js.map +1 -0
- package/package.json +34 -0
- package/rollup.config.cjs +36 -0
- package/src/components/BondingCurve/BondingCurve.js +296 -0
- package/src/components/Display/BalanceDisplay.js +81 -0
- package/src/components/Display/PriceDisplay.js +214 -0
- package/src/components/Ipfs/IpfsImage.js +265 -0
- package/src/components/Modal/ApprovalModal.js +398 -0
- package/src/components/Swap/SwapButton.js +81 -0
- package/src/components/Swap/SwapInputs.js +137 -0
- package/src/components/Swap/SwapInterface.js +972 -0
- package/src/components/Swap/TransactionOptions.js +238 -0
- package/src/components/Util/MessagePopup.js +159 -0
- package/src/components/Wallet/WalletModal.js +69 -0
- package/src/components/Wallet/WalletSplash.js +567 -0
- package/src/index.js +43 -0
- package/src/services/BlockchainService.js +1576 -0
- package/src/services/ContractCache.js +348 -0
- package/src/services/IpfsService.js +249 -0
- package/src/services/PriceService.js +191 -0
- package/src/services/WalletService.js +541 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
class ContractCache {
|
|
2
|
+
constructor(eventBus) {
|
|
3
|
+
if (!eventBus) {
|
|
4
|
+
throw new Error('ContractCache requires an eventBus instance.');
|
|
5
|
+
}
|
|
6
|
+
this.eventBus = eventBus;
|
|
7
|
+
// Cache storage: Map<key, {value, expiresAt, accessCount}>
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
|
|
10
|
+
// Cache statistics
|
|
11
|
+
this.stats = {
|
|
12
|
+
hits: 0,
|
|
13
|
+
misses: 0,
|
|
14
|
+
invalidations: 0,
|
|
15
|
+
sets: 0
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Default TTL per method type (in milliseconds)
|
|
19
|
+
this.defaultTTLs = {
|
|
20
|
+
// Price data changes frequently but can be cached briefly
|
|
21
|
+
price: 5000, // 5 seconds
|
|
22
|
+
// Balances should be fresh but can cache briefly
|
|
23
|
+
balance: 3000, // 3 seconds
|
|
24
|
+
// Contract data changes less frequently
|
|
25
|
+
contractData: 10000, // 10 seconds
|
|
26
|
+
// Supply data changes less frequently
|
|
27
|
+
supply: 10000, // 10 seconds
|
|
28
|
+
// NFT metadata rarely changes
|
|
29
|
+
metadata: 60000, // 60 seconds
|
|
30
|
+
// Tier/whitelist data changes infrequently
|
|
31
|
+
tier: 30000, // 30 seconds
|
|
32
|
+
// Default TTL
|
|
33
|
+
default: 5000 // 5 seconds
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Methods that should never be cached
|
|
37
|
+
this.noCacheMethods = new Set([
|
|
38
|
+
'executeContractCall', // Transactions
|
|
39
|
+
'swapExactETHForTokens',
|
|
40
|
+
'swapExactTokensForETH',
|
|
41
|
+
'buy',
|
|
42
|
+
'sell',
|
|
43
|
+
'mint'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Setup event listeners for cache invalidation
|
|
47
|
+
this.setupEventListeners();
|
|
48
|
+
|
|
49
|
+
// Debug mode
|
|
50
|
+
this.debug = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Setup event listeners for automatic cache invalidation
|
|
55
|
+
*/
|
|
56
|
+
setupEventListeners() {
|
|
57
|
+
// Invalidate on transaction confirmation
|
|
58
|
+
this.eventBus.on('transaction:confirmed', () => {
|
|
59
|
+
this.invalidateByPattern('balance', 'price', 'contractData');
|
|
60
|
+
if (this.debug) {
|
|
61
|
+
console.log('[ContractCache] Invalidated cache due to transaction confirmation');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Invalidate on account change
|
|
66
|
+
this.eventBus.on('account:changed', () => {
|
|
67
|
+
this.invalidateByPattern('balance');
|
|
68
|
+
if (this.debug) {
|
|
69
|
+
console.log('[ContractCache] Invalidated cache due to account change');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Invalidate on network change
|
|
74
|
+
this.eventBus.on('network:changed', () => {
|
|
75
|
+
this.clear(); // Clear all cache on network change
|
|
76
|
+
if (this.debug) {
|
|
77
|
+
console.log('[ContractCache] Cleared cache due to network change');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Invalidate on contract data update
|
|
82
|
+
this.eventBus.on('contractData:updated', () => {
|
|
83
|
+
this.invalidateByPattern('contractData', 'price', 'supply');
|
|
84
|
+
if (this.debug) {
|
|
85
|
+
console.log('[ContractCache] Invalidated cache due to contract data update');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate cache key from method name and arguments
|
|
92
|
+
* @param {string} method - Method name
|
|
93
|
+
* @param {Array} args - Method arguments
|
|
94
|
+
* @returns {string} - Cache key
|
|
95
|
+
*/
|
|
96
|
+
generateKey(method, args = []) {
|
|
97
|
+
// Create a stable key from method and args
|
|
98
|
+
const argsKey = args.length > 0
|
|
99
|
+
? JSON.stringify(args.map(arg => {
|
|
100
|
+
// Handle BigNumber and other special types
|
|
101
|
+
if (arg && typeof arg === 'object' && arg.toString) {
|
|
102
|
+
return arg.toString();
|
|
103
|
+
}
|
|
104
|
+
return arg;
|
|
105
|
+
}))
|
|
106
|
+
: '';
|
|
107
|
+
return `${method}:${argsKey}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get cached value if available and not expired
|
|
112
|
+
* @param {string} method - Method name
|
|
113
|
+
* @param {Array} args - Method arguments
|
|
114
|
+
* @returns {any|null} - Cached value or null if not found/expired
|
|
115
|
+
*/
|
|
116
|
+
get(method, args = []) {
|
|
117
|
+
// Don't cache certain methods
|
|
118
|
+
if (this.noCacheMethods.has(method)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const key = this.generateKey(method, args);
|
|
123
|
+
const entry = this.cache.get(key);
|
|
124
|
+
|
|
125
|
+
if (!entry) {
|
|
126
|
+
this.stats.misses++;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if expired
|
|
131
|
+
if (Date.now() > entry.expiresAt) {
|
|
132
|
+
this.cache.delete(key);
|
|
133
|
+
this.stats.misses++;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Update access count
|
|
138
|
+
entry.accessCount++;
|
|
139
|
+
this.stats.hits++;
|
|
140
|
+
|
|
141
|
+
if (this.debug) {
|
|
142
|
+
console.log(`[ContractCache] Cache HIT for ${method}`, args);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return entry.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set cached value with TTL
|
|
150
|
+
* @param {string} method - Method name
|
|
151
|
+
* @param {Array} args - Method arguments
|
|
152
|
+
* @param {any} value - Value to cache
|
|
153
|
+
* @param {number} ttl - Time to live in milliseconds (optional, uses default if not provided)
|
|
154
|
+
*/
|
|
155
|
+
set(method, args = [], value, ttl = null) {
|
|
156
|
+
// Don't cache certain methods
|
|
157
|
+
if (this.noCacheMethods.has(method)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Determine TTL based on method type
|
|
162
|
+
if (ttl === null) {
|
|
163
|
+
ttl = this.getTTLForMethod(method);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const key = this.generateKey(method, args);
|
|
167
|
+
const expiresAt = Date.now() + ttl;
|
|
168
|
+
|
|
169
|
+
this.cache.set(key, {
|
|
170
|
+
value,
|
|
171
|
+
expiresAt,
|
|
172
|
+
accessCount: 0,
|
|
173
|
+
method,
|
|
174
|
+
args,
|
|
175
|
+
cachedAt: Date.now()
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.stats.sets++;
|
|
179
|
+
|
|
180
|
+
if (this.debug) {
|
|
181
|
+
console.log(`[ContractCache] Cached ${method}`, args, `TTL: ${ttl}ms`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get TTL for a method based on its type
|
|
187
|
+
* @param {string} method - Method name
|
|
188
|
+
* @returns {number} - TTL in milliseconds
|
|
189
|
+
*/
|
|
190
|
+
getTTLForMethod(method) {
|
|
191
|
+
const methodLower = method.toLowerCase();
|
|
192
|
+
|
|
193
|
+
// Price-related methods
|
|
194
|
+
if (methodLower.includes('price') || methodLower.includes('cost')) {
|
|
195
|
+
return this.defaultTTLs.price;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Balance-related methods
|
|
199
|
+
if (methodLower.includes('balance')) {
|
|
200
|
+
return this.defaultTTLs.balance;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Supply-related methods
|
|
204
|
+
if (methodLower.includes('supply') || methodLower.includes('total')) {
|
|
205
|
+
return this.defaultTTLs.supply;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Metadata-related methods
|
|
209
|
+
if (methodLower.includes('metadata') || methodLower.includes('uri')) {
|
|
210
|
+
return this.defaultTTLs.metadata;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Tier/whitelist-related methods
|
|
214
|
+
if (methodLower.includes('tier') || methodLower.includes('merkle') || methodLower.includes('proof')) {
|
|
215
|
+
return this.defaultTTLs.tier;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Contract data methods
|
|
219
|
+
if (methodLower.includes('contract') || methodLower.includes('pool') || methodLower.includes('liquidity')) {
|
|
220
|
+
return this.defaultTTLs.contractData;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return this.defaultTTLs.default;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Invalidate cache entries matching patterns
|
|
228
|
+
* @param {...string} patterns - Patterns to match against method names
|
|
229
|
+
*/
|
|
230
|
+
invalidateByPattern(...patterns) {
|
|
231
|
+
let invalidated = 0;
|
|
232
|
+
|
|
233
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
234
|
+
const methodLower = entry.method.toLowerCase();
|
|
235
|
+
|
|
236
|
+
if (patterns.some(pattern => methodLower.includes(pattern.toLowerCase()))) {
|
|
237
|
+
this.cache.delete(key);
|
|
238
|
+
invalidated++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.stats.invalidations += invalidated;
|
|
243
|
+
|
|
244
|
+
if (this.debug && invalidated > 0) {
|
|
245
|
+
console.log(`[ContractCache] Invalidated ${invalidated} entries matching patterns:`, patterns);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Invalidate specific cache entry
|
|
251
|
+
* @param {string} method - Method name
|
|
252
|
+
* @param {Array} args - Method arguments
|
|
253
|
+
*/
|
|
254
|
+
invalidate(method, args = []) {
|
|
255
|
+
const key = this.generateKey(method, args);
|
|
256
|
+
if (this.cache.delete(key)) {
|
|
257
|
+
this.stats.invalidations++;
|
|
258
|
+
if (this.debug) {
|
|
259
|
+
console.log(`[ContractCache] Invalidated ${method}`, args);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Clear all cache
|
|
266
|
+
*/
|
|
267
|
+
clear() {
|
|
268
|
+
const size = this.cache.size;
|
|
269
|
+
this.cache.clear();
|
|
270
|
+
this.stats.invalidations += size;
|
|
271
|
+
|
|
272
|
+
if (this.debug) {
|
|
273
|
+
console.log('[ContractCache] Cleared all cache');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get cache statistics
|
|
279
|
+
* @returns {Object} - Cache statistics
|
|
280
|
+
*/
|
|
281
|
+
getStats() {
|
|
282
|
+
const total = this.stats.hits + this.stats.misses;
|
|
283
|
+
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(2) : 0;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...this.stats,
|
|
287
|
+
hitRate: `${hitRate}%`,
|
|
288
|
+
cacheSize: this.cache.size,
|
|
289
|
+
totalRequests: total
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Warm cache by pre-fetching common data
|
|
295
|
+
* @param {Function} fetchFn - Function to fetch data
|
|
296
|
+
* @param {Array} methods - Methods to warm
|
|
297
|
+
*/
|
|
298
|
+
async warmCache(fetchFn, methods = []) {
|
|
299
|
+
if (this.debug) {
|
|
300
|
+
console.log('[ContractCache] Warming cache for methods:', methods);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const promises = methods.map(async (method) => {
|
|
304
|
+
try {
|
|
305
|
+
const result = await fetchFn(method);
|
|
306
|
+
// Cache will be set by the wrapped method
|
|
307
|
+
return result;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn(`[ContractCache] Failed to warm cache for ${method}:`, error);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await Promise.all(promises);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Enable or disable debug logging
|
|
319
|
+
* @param {boolean} enabled - Whether to enable debug mode
|
|
320
|
+
*/
|
|
321
|
+
setDebug(enabled) {
|
|
322
|
+
this.debug = enabled;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Clean up expired entries (should be called periodically)
|
|
327
|
+
*/
|
|
328
|
+
cleanup() {
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
let cleaned = 0;
|
|
331
|
+
|
|
332
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
333
|
+
if (now > entry.expiresAt) {
|
|
334
|
+
this.cache.delete(key);
|
|
335
|
+
cleaned++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.debug && cleaned > 0) {
|
|
340
|
+
console.log(`[ContractCache] Cleaned up ${cleaned} expired entries`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return cleaned;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export default ContractCache;
|
|
348
|
+
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPFS Service
|
|
3
|
+
*
|
|
4
|
+
* Light client for IPFS resolution with gateway rotation and fallback.
|
|
5
|
+
* Handles both direct IPFS URIs and metadata fetching.
|
|
6
|
+
*
|
|
7
|
+
* No secrets, no backend - pure client-side IPFS gateway resolution.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Public IPFS gateways (in order of preference)
|
|
12
|
+
* These are public gateways that don't require authentication
|
|
13
|
+
*/
|
|
14
|
+
const IPFS_GATEWAYS = [
|
|
15
|
+
'https://w3s.link/ipfs/',
|
|
16
|
+
'https://cloudflare-ipfs.com/ipfs/',
|
|
17
|
+
'https://ipfs.io/ipfs/',
|
|
18
|
+
'https://gateway.pinata.cloud/ipfs/',
|
|
19
|
+
'https://dweb.link/ipfs/'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get custom IPFS gateway from localStorage (user preference)
|
|
24
|
+
* @returns {string|null} Custom gateway base URL or null
|
|
25
|
+
*/
|
|
26
|
+
function getCustomGateway() {
|
|
27
|
+
try {
|
|
28
|
+
const custom = localStorage.getItem('customIpfsGatewayBaseUrl');
|
|
29
|
+
if (custom && typeof custom === 'string' && custom.trim()) {
|
|
30
|
+
const trimmed = custom.trim();
|
|
31
|
+
// Ensure it ends with /ipfs/ or just /
|
|
32
|
+
if (trimmed.endsWith('/ipfs/')) {
|
|
33
|
+
return trimmed;
|
|
34
|
+
} else if (trimmed.endsWith('/')) {
|
|
35
|
+
return trimmed + 'ipfs/';
|
|
36
|
+
} else {
|
|
37
|
+
return trimmed + '/ipfs/';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn('[IpfsService] Failed to read custom gateway from localStorage:', error);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all available gateways (custom first, then public list)
|
|
48
|
+
* @returns {string[]} Array of gateway base URLs
|
|
49
|
+
*/
|
|
50
|
+
function getAllGateways() {
|
|
51
|
+
const custom = getCustomGateway();
|
|
52
|
+
return custom ? [custom, ...IPFS_GATEWAYS] : IPFS_GATEWAYS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a URI is an IPFS URI
|
|
57
|
+
* @param {string} uri - URI to check
|
|
58
|
+
* @returns {boolean} True if URI is IPFS
|
|
59
|
+
*/
|
|
60
|
+
export function isIpfsUri(uri) {
|
|
61
|
+
if (!uri || typeof uri !== 'string') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return uri.toLowerCase().startsWith('ipfs://');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normalize IPFS path by removing ipfs:// prefix
|
|
69
|
+
* Handles both ipfs://CID and ipfs://CID/path formats
|
|
70
|
+
* @param {string} ipfsUri - IPFS URI (e.g., ipfs://Qm... or ipfs://Qm.../path)
|
|
71
|
+
* @returns {string} Normalized path (e.g., Qm... or Qm.../path)
|
|
72
|
+
*/
|
|
73
|
+
export function normalizeIpfsPath(ipfsUri) {
|
|
74
|
+
if (!isIpfsUri(ipfsUri)) {
|
|
75
|
+
return ipfsUri;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Remove ipfs:// prefix
|
|
79
|
+
const path = ipfsUri.substring(7); // 'ipfs://' is 7 characters
|
|
80
|
+
|
|
81
|
+
// Remove leading slash if present
|
|
82
|
+
return path.startsWith('/') ? path.substring(1) : path;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve IPFS URI to HTTP URL using a specific gateway
|
|
87
|
+
* @param {string} ipfsUri - IPFS URI (e.g., ipfs://Qm...)
|
|
88
|
+
* @param {number} gatewayIndex - Index of gateway to use (0-based)
|
|
89
|
+
* @returns {string|null} HTTP URL or null if invalid
|
|
90
|
+
*/
|
|
91
|
+
export function resolveIpfsToHttp(ipfsUri, gatewayIndex = 0) {
|
|
92
|
+
if (!isIpfsUri(ipfsUri)) {
|
|
93
|
+
return ipfsUri; // Return as-is if not IPFS
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const gateways = getAllGateways();
|
|
97
|
+
if (gatewayIndex < 0 || gatewayIndex >= gateways.length) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const normalizedPath = normalizeIpfsPath(ipfsUri);
|
|
102
|
+
const gateway = gateways[gatewayIndex];
|
|
103
|
+
|
|
104
|
+
return gateway + normalizedPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch JSON from IPFS or HTTP URL with gateway rotation
|
|
109
|
+
* @param {string} urlOrIpfs - HTTP URL or IPFS URI
|
|
110
|
+
* @param {object} options - Fetch options (timeout, etc.)
|
|
111
|
+
* @returns {Promise<object>} Parsed JSON data
|
|
112
|
+
* @throws {Error} If all gateways fail or JSON is invalid
|
|
113
|
+
*/
|
|
114
|
+
export async function fetchJsonWithIpfsSupport(urlOrIpfs, options = {}) {
|
|
115
|
+
const { timeout = 10000, ...fetchOptions } = options;
|
|
116
|
+
|
|
117
|
+
// If it's not an IPFS URI, use regular fetch
|
|
118
|
+
if (!isIpfsUri(urlOrIpfs)) {
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(urlOrIpfs, {
|
|
124
|
+
...fetchOptions,
|
|
125
|
+
signal: controller.signal
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const json = await response.json();
|
|
133
|
+
clearTimeout(timeoutId);
|
|
134
|
+
return json;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
clearTimeout(timeoutId);
|
|
137
|
+
if (error.name === 'AbortError') {
|
|
138
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// IPFS URI - try gateways in order
|
|
145
|
+
const gateways = getAllGateways();
|
|
146
|
+
const normalizedPath = normalizeIpfsPath(urlOrIpfs);
|
|
147
|
+
const errors = [];
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < gateways.length; i++) {
|
|
150
|
+
const gateway = gateways[i];
|
|
151
|
+
const httpUrl = gateway + normalizedPath;
|
|
152
|
+
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(httpUrl, {
|
|
158
|
+
...fetchOptions,
|
|
159
|
+
signal: controller.signal
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const json = await response.json();
|
|
169
|
+
|
|
170
|
+
// Success - log which gateway worked (for debugging)
|
|
171
|
+
if (i > 0) {
|
|
172
|
+
console.log(`[IpfsService] Resolved IPFS via gateway ${i + 1}/${gateways.length}: ${gateway}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return json;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
clearTimeout(timeoutId);
|
|
178
|
+
|
|
179
|
+
const errorMsg = error.name === 'AbortError'
|
|
180
|
+
? `Timeout after ${timeout}ms`
|
|
181
|
+
: error.message || 'Unknown error';
|
|
182
|
+
|
|
183
|
+
errors.push(`Gateway ${i + 1} (${gateway}): ${errorMsg}`);
|
|
184
|
+
|
|
185
|
+
// Continue to next gateway
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// All gateways failed
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Failed to fetch IPFS content after trying ${gateways.length} gateways:\n` +
|
|
193
|
+
errors.join('\n')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Test if an image URL can be loaded (for gateway rotation)
|
|
199
|
+
* @param {string} url - HTTP URL to test
|
|
200
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
201
|
+
* @returns {Promise<boolean>} True if image loads successfully
|
|
202
|
+
*/
|
|
203
|
+
export function testImageLoad(url, timeout = 5000) {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
const img = new Image();
|
|
206
|
+
const timeoutId = setTimeout(() => {
|
|
207
|
+
img.onload = null;
|
|
208
|
+
img.onerror = null;
|
|
209
|
+
resolve(false);
|
|
210
|
+
}, timeout);
|
|
211
|
+
|
|
212
|
+
img.onload = () => {
|
|
213
|
+
clearTimeout(timeoutId);
|
|
214
|
+
resolve(true);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
img.onerror = () => {
|
|
218
|
+
clearTimeout(timeoutId);
|
|
219
|
+
resolve(false);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
img.src = url;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all available gateways (for UI display or debugging)
|
|
228
|
+
* @returns {string[]} Array of gateway base URLs
|
|
229
|
+
*/
|
|
230
|
+
export function getAvailableGateways() {
|
|
231
|
+
return getAllGateways();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set custom IPFS gateway (user preference)
|
|
236
|
+
* @param {string} gatewayBaseUrl - Base URL of custom gateway (e.g., 'https://mygateway.com/ipfs/')
|
|
237
|
+
*/
|
|
238
|
+
export function setCustomGateway(gatewayBaseUrl) {
|
|
239
|
+
try {
|
|
240
|
+
if (gatewayBaseUrl) {
|
|
241
|
+
localStorage.setItem('customIpfsGatewayBaseUrl', gatewayBaseUrl);
|
|
242
|
+
} else {
|
|
243
|
+
localStorage.removeItem('customIpfsGatewayBaseUrl');
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.warn('[IpfsService] Failed to save custom gateway to localStorage:', error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|