curvance 4.0.3 → 4.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.
Files changed (111) hide show
  1. package/README.md +595 -59
  2. package/dist/chains/arbitrum.d.ts.map +1 -1
  3. package/dist/chains/arbitrum.js +4 -2
  4. package/dist/chains/arbitrum.js.map +1 -1
  5. package/dist/chains/index.d.ts +4 -0
  6. package/dist/chains/index.d.ts.map +1 -1
  7. package/dist/chains/index.js +15 -0
  8. package/dist/chains/index.js.map +1 -1
  9. package/dist/chains/monad.d.ts.map +1 -1
  10. package/dist/chains/monad.js +4 -2
  11. package/dist/chains/monad.js.map +1 -1
  12. package/dist/chains/rpc.d.ts +57 -0
  13. package/dist/chains/rpc.d.ts.map +1 -0
  14. package/dist/chains/rpc.js +47 -0
  15. package/dist/chains/rpc.js.map +1 -0
  16. package/dist/classes/Api.d.ts +4 -3
  17. package/dist/classes/Api.d.ts.map +1 -1
  18. package/dist/classes/Api.js +7 -7
  19. package/dist/classes/Api.js.map +1 -1
  20. package/dist/classes/BorrowableCToken.d.ts +3 -2
  21. package/dist/classes/BorrowableCToken.d.ts.map +1 -1
  22. package/dist/classes/BorrowableCToken.js +6 -5
  23. package/dist/classes/BorrowableCToken.js.map +1 -1
  24. package/dist/classes/CToken.d.ts +11 -3
  25. package/dist/classes/CToken.d.ts.map +1 -1
  26. package/dist/classes/CToken.js +168 -116
  27. package/dist/classes/CToken.js.map +1 -1
  28. package/dist/classes/Calldata.d.ts +2 -2
  29. package/dist/classes/Calldata.d.ts.map +1 -1
  30. package/dist/classes/Calldata.js +2 -2
  31. package/dist/classes/Calldata.js.map +1 -1
  32. package/dist/classes/DexAggregators/IDexAgg.d.ts +2 -2
  33. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  34. package/dist/classes/DexAggregators/Kuru.d.ts +2 -2
  35. package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
  36. package/dist/classes/DexAggregators/Kuru.js +3 -4
  37. package/dist/classes/DexAggregators/Kuru.js.map +1 -1
  38. package/dist/classes/DexAggregators/KyberSwap.d.ts +2 -2
  39. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  40. package/dist/classes/DexAggregators/KyberSwap.js +88 -9
  41. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  42. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +2 -2
  43. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  44. package/dist/classes/DexAggregators/MultiDexAgg.js +3 -3
  45. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  46. package/dist/classes/ERC20.d.ts +5 -3
  47. package/dist/classes/ERC20.d.ts.map +1 -1
  48. package/dist/classes/ERC20.js +20 -14
  49. package/dist/classes/ERC20.js.map +1 -1
  50. package/dist/classes/ERC4626.d.ts.map +1 -1
  51. package/dist/classes/ERC4626.js +3 -1
  52. package/dist/classes/ERC4626.js.map +1 -1
  53. package/dist/classes/Market.d.ts +13 -4
  54. package/dist/classes/Market.d.ts.map +1 -1
  55. package/dist/classes/Market.js +86 -28
  56. package/dist/classes/Market.js.map +1 -1
  57. package/dist/classes/NativeToken.d.ts +6 -3
  58. package/dist/classes/NativeToken.d.ts.map +1 -1
  59. package/dist/classes/NativeToken.js +11 -16
  60. package/dist/classes/NativeToken.js.map +1 -1
  61. package/dist/classes/OptimizerReader.d.ts +3 -3
  62. package/dist/classes/OptimizerReader.d.ts.map +1 -1
  63. package/dist/classes/OptimizerReader.js +1 -1
  64. package/dist/classes/OptimizerReader.js.map +1 -1
  65. package/dist/classes/OracleManager.d.ts +3 -3
  66. package/dist/classes/OracleManager.d.ts.map +1 -1
  67. package/dist/classes/OracleManager.js +1 -1
  68. package/dist/classes/OracleManager.js.map +1 -1
  69. package/dist/classes/PositionManager.d.ts +2 -2
  70. package/dist/classes/PositionManager.d.ts.map +1 -1
  71. package/dist/classes/PositionManager.js +4 -4
  72. package/dist/classes/PositionManager.js.map +1 -1
  73. package/dist/classes/ProtocolReader.d.ts +18 -4
  74. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  75. package/dist/classes/ProtocolReader.js +177 -55
  76. package/dist/classes/ProtocolReader.js.map +1 -1
  77. package/dist/classes/Redstone.d.ts.map +1 -1
  78. package/dist/classes/Redstone.js +1 -2
  79. package/dist/classes/Redstone.js.map +1 -1
  80. package/dist/classes/Zapper.d.ts +4 -2
  81. package/dist/classes/Zapper.d.ts.map +1 -1
  82. package/dist/classes/Zapper.js +16 -14
  83. package/dist/classes/Zapper.js.map +1 -1
  84. package/dist/classes/index.d.ts +1 -1
  85. package/dist/classes/index.d.ts.map +1 -1
  86. package/dist/classes/index.js +6 -1
  87. package/dist/classes/index.js.map +1 -1
  88. package/dist/contracts/monad-mainnet.json +1 -1
  89. package/dist/feePolicy.d.ts +27 -1
  90. package/dist/feePolicy.d.ts.map +1 -1
  91. package/dist/feePolicy.js +10 -2
  92. package/dist/feePolicy.js.map +1 -1
  93. package/dist/helpers.d.ts +3 -1
  94. package/dist/helpers.d.ts.map +1 -1
  95. package/dist/helpers.js +34 -4
  96. package/dist/helpers.js.map +1 -1
  97. package/dist/integrations/snapshot.d.ts.map +1 -1
  98. package/dist/integrations/snapshot.js +4 -18
  99. package/dist/integrations/snapshot.js.map +1 -1
  100. package/dist/retry-provider.d.ts +81 -6
  101. package/dist/retry-provider.d.ts.map +1 -1
  102. package/dist/retry-provider.js +491 -37
  103. package/dist/retry-provider.js.map +1 -1
  104. package/dist/setup.d.ts +14 -3
  105. package/dist/setup.d.ts.map +1 -1
  106. package/dist/setup.js +56 -20
  107. package/dist/setup.js.map +1 -1
  108. package/dist/types.d.ts +2 -1
  109. package/dist/types.d.ts.map +1 -1
  110. package/dist/types.js.map +1 -1
  111. package/package.json +3 -1
@@ -1,16 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RetryableProvider = exports.DEFAULT_RETRY_CONFIG = void 0;
4
+ exports.getRpcDebugSnapshot = getRpcDebugSnapshot;
5
+ exports.subscribeToRpcDebug = subscribeToRpcDebug;
6
+ exports.resetRpcDebugState = resetRpcDebugState;
4
7
  exports.configureRetries = configureRetries;
5
8
  exports.createRetryableProvider = createRetryableProvider;
6
9
  exports.wrapProviderWithRetries = wrapProviderWithRetries;
7
10
  exports.isRetryableProvider = isRetryableProvider;
11
+ exports.isRetryableReadProvider = isRetryableReadProvider;
8
12
  exports.classifyError = classifyError;
13
+ const rpc_1 = require("./chains/rpc");
14
+ /** Named ethers provider methods that make RPC calls on the read transport. */
15
+ const RPC_PROVIDER_METHODS = new Set([
16
+ 'getBalance',
17
+ 'getCode',
18
+ 'getStorageAt',
19
+ 'getTransactionCount',
20
+ 'getBlock',
21
+ 'getBlockNumber',
22
+ 'getGasPrice',
23
+ 'getFeeData',
24
+ 'getTransaction',
25
+ 'getTransactionReceipt',
26
+ 'getLogs',
27
+ 'getNetwork',
28
+ 'detectNetwork',
29
+ 'call',
30
+ 'estimateGas',
31
+ ]);
9
32
  exports.DEFAULT_RETRY_CONFIG = {
10
- maxRetries: 3,
11
- baseDelay: 1000, // 1 second
12
- maxDelay: 10000, // 10 seconds
33
+ maxRetries: rpc_1.DEFAULT_CHAIN_RPC_POLICY.retryCount,
34
+ baseDelay: rpc_1.DEFAULT_CHAIN_RPC_POLICY.retryDelayMs,
35
+ maxDelay: 1000,
13
36
  backoffMultiplier: 2,
37
+ timeoutMs: rpc_1.DEFAULT_CHAIN_RPC_POLICY.timeoutMs,
38
+ fallbackCooldownMs: rpc_1.DEFAULT_CHAIN_RPC_POLICY.fallbackCooldownMs,
39
+ rankSampleCount: rpc_1.DEFAULT_CHAIN_RPC_POLICY.rankSampleCount,
40
+ rankWeights: rpc_1.DEFAULT_CHAIN_RPC_POLICY.rankWeights,
14
41
  retryableErrors: [
15
42
  // Rate limiting
16
43
  'rate limit',
@@ -53,10 +80,71 @@ exports.DEFAULT_RETRY_CONFIG = {
53
80
  'try again'
54
81
  ]
55
82
  };
83
+ const rpcDebugListeners = new Set();
84
+ const rpcDebugStates = new Map();
85
+ function normalizeRpcUrl(url) {
86
+ if (!url) {
87
+ return null;
88
+ }
89
+ return url.replace(/\/+$/, '');
90
+ }
91
+ function getProviderUrl(provider) {
92
+ const connection = provider._getConnection?.() ?? provider.connection ?? null;
93
+ return normalizeRpcUrl(connection?.url);
94
+ }
95
+ function getEndpointId(url, label) {
96
+ return url ?? label;
97
+ }
98
+ function cloneRpcDebugState(state) {
99
+ return { ...state };
100
+ }
101
+ function emitRpcDebugSnapshot() {
102
+ if (rpcDebugListeners.size === 0) {
103
+ return;
104
+ }
105
+ const snapshot = getRpcDebugSnapshot();
106
+ for (const listener of rpcDebugListeners) {
107
+ listener(snapshot);
108
+ }
109
+ }
110
+ function getRpcDebugSnapshot() {
111
+ return {
112
+ updatedAt: Date.now(),
113
+ endpoints: [...rpcDebugStates.values()]
114
+ .map(cloneRpcDebugState)
115
+ .sort((a, b) => {
116
+ if (a.role !== b.role) {
117
+ return a.role === 'primary' ? -1 : 1;
118
+ }
119
+ return a.label.localeCompare(b.label);
120
+ }),
121
+ };
122
+ }
123
+ function subscribeToRpcDebug(listener) {
124
+ rpcDebugListeners.add(listener);
125
+ listener(getRpcDebugSnapshot());
126
+ return () => {
127
+ rpcDebugListeners.delete(listener);
128
+ };
129
+ }
130
+ function resetRpcDebugState() {
131
+ rpcDebugStates.clear();
132
+ emitRpcDebugSnapshot();
133
+ }
56
134
  class RetryableProvider {
57
135
  config;
58
- constructor(config = {}) {
136
+ fallbackProviders;
137
+ fallbackProviderStates;
138
+ _fallbackActivated = false;
139
+ primaryReadCooldownUntil = 0;
140
+ constructor(config = {}, fallbackProviders = null) {
59
141
  this.config = { ...exports.DEFAULT_RETRY_CONFIG, ...config };
142
+ this.fallbackProviders = Array.isArray(fallbackProviders)
143
+ ? fallbackProviders
144
+ : fallbackProviders
145
+ ? [fallbackProviders]
146
+ : [];
147
+ this.fallbackProviderStates = this.fallbackProviders.map((provider, index) => this.createProviderState(provider, 'fallback', `fallback-${index + 1}`, index));
60
148
  }
61
149
  async sleep(ms) {
62
150
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -67,6 +155,166 @@ class RetryableProvider {
67
155
  // Add jitter to prevent thundering herd when multiple clients retry simultaneously
68
156
  return capped * (0.5 + Math.random() * 0.5);
69
157
  }
158
+ createProviderState(provider, role, label, index) {
159
+ const url = getProviderUrl(provider);
160
+ const state = {
161
+ provider,
162
+ label,
163
+ role,
164
+ endpointId: getEndpointId(url, label),
165
+ url,
166
+ index,
167
+ cooldownUntil: 0,
168
+ lastFailureAt: 0,
169
+ lastSuccessAt: 0,
170
+ attempts: 0,
171
+ successes: 0,
172
+ retryableFailures: 0,
173
+ nonRetryableFailures: 0,
174
+ timeoutFailures: 0,
175
+ fallbackSelections: 0,
176
+ lastLatencyMs: null,
177
+ lastError: null,
178
+ lastAttemptAt: 0,
179
+ recentSamples: [],
180
+ };
181
+ this.publishDebugState(state);
182
+ return state;
183
+ }
184
+ getRecentSamples(state) {
185
+ return state.recentSamples.slice(-this.config.rankSampleCount);
186
+ }
187
+ getProviderSuccessRate(state) {
188
+ const samples = this.getRecentSamples(state);
189
+ if (samples.length === 0) {
190
+ return 0.5;
191
+ }
192
+ const successes = samples.filter((sample) => sample.success).length;
193
+ return successes / samples.length;
194
+ }
195
+ getProviderAverageLatency(state) {
196
+ const latencies = this.getRecentSamples(state)
197
+ .filter((sample) => sample.success && sample.latencyMs != null)
198
+ .map((sample) => sample.latencyMs);
199
+ if (latencies.length === 0) {
200
+ return null;
201
+ }
202
+ return latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length;
203
+ }
204
+ getProviderRankScore(state) {
205
+ const averageLatencyMs = this.getProviderAverageLatency(state);
206
+ const latencyScore = averageLatencyMs == null
207
+ ? 0.5
208
+ : Math.max(0, 1 - Math.min(averageLatencyMs, this.config.timeoutMs) / this.config.timeoutMs);
209
+ const stabilityScore = this.getProviderSuccessRate(state);
210
+ return ((latencyScore * this.config.rankWeights.latency) +
211
+ (stabilityScore * this.config.rankWeights.stability));
212
+ }
213
+ publishDebugState(state) {
214
+ const current = rpcDebugStates.get(state.endpointId) ?? {
215
+ endpointId: state.endpointId,
216
+ label: state.label,
217
+ role: state.role,
218
+ url: state.url,
219
+ attempts: 0,
220
+ successes: 0,
221
+ retryableFailures: 0,
222
+ nonRetryableFailures: 0,
223
+ timeoutFailures: 0,
224
+ fallbackSelections: 0,
225
+ recentSampleCount: 0,
226
+ recentSuccessRate: null,
227
+ averageLatencyMs: null,
228
+ lastLatencyMs: null,
229
+ rankScore: null,
230
+ lastError: null,
231
+ lastAttemptAt: null,
232
+ lastSuccessAt: null,
233
+ lastFailureAt: null,
234
+ cooldownUntil: null,
235
+ };
236
+ current.label = state.label;
237
+ current.role = state.role;
238
+ current.url = state.url;
239
+ current.recentSampleCount = this.getRecentSamples(state).length;
240
+ current.recentSuccessRate = current.recentSampleCount > 0 ? this.getProviderSuccessRate(state) : null;
241
+ current.averageLatencyMs = this.getProviderAverageLatency(state);
242
+ current.lastLatencyMs = state.lastLatencyMs;
243
+ current.rankScore = this.getProviderRankScore(state);
244
+ current.lastError = state.lastError;
245
+ current.lastAttemptAt = state.lastAttemptAt || current.lastAttemptAt;
246
+ current.lastSuccessAt = state.lastSuccessAt || current.lastSuccessAt;
247
+ current.lastFailureAt = state.lastFailureAt || current.lastFailureAt;
248
+ current.cooldownUntil = state.cooldownUntil || null;
249
+ rpcDebugStates.set(state.endpointId, current);
250
+ emitRpcDebugSnapshot();
251
+ }
252
+ recordProviderSelection(state) {
253
+ state.fallbackSelections += 1;
254
+ const current = rpcDebugStates.get(state.endpointId);
255
+ if (current) {
256
+ current.fallbackSelections += 1;
257
+ }
258
+ this.publishDebugState(state);
259
+ }
260
+ recordProviderAttempt(state, latencyMs, error) {
261
+ const recordedAt = Date.now();
262
+ state.attempts += 1;
263
+ state.lastAttemptAt = recordedAt;
264
+ state.lastLatencyMs = latencyMs;
265
+ state.recentSamples.push({
266
+ success: error == null,
267
+ latencyMs,
268
+ });
269
+ if (state.recentSamples.length > this.config.rankSampleCount) {
270
+ state.recentSamples.shift();
271
+ }
272
+ const current = rpcDebugStates.get(state.endpointId);
273
+ if (current) {
274
+ current.attempts += 1;
275
+ current.lastAttemptAt = recordedAt;
276
+ current.lastLatencyMs = latencyMs;
277
+ }
278
+ if (error == null) {
279
+ state.successes += 1;
280
+ state.lastSuccessAt = recordedAt;
281
+ state.lastError = null;
282
+ if (current) {
283
+ current.successes += 1;
284
+ current.lastSuccessAt = recordedAt;
285
+ current.lastError = null;
286
+ }
287
+ this.publishDebugState(state);
288
+ return;
289
+ }
290
+ const isTimeout = error?.code === 'timeout' || String(error?.message ?? '').toLowerCase().includes('timeout');
291
+ const isRetryable = this.isRetryableError(error);
292
+ state.lastFailureAt = recordedAt;
293
+ state.lastError = error?.message ?? String(error);
294
+ if (isRetryable) {
295
+ state.retryableFailures += 1;
296
+ }
297
+ else {
298
+ state.nonRetryableFailures += 1;
299
+ }
300
+ if (isTimeout) {
301
+ state.timeoutFailures += 1;
302
+ }
303
+ if (current) {
304
+ current.lastFailureAt = recordedAt;
305
+ current.lastError = state.lastError;
306
+ if (isRetryable) {
307
+ current.retryableFailures += 1;
308
+ }
309
+ else {
310
+ current.nonRetryableFailures += 1;
311
+ }
312
+ if (isTimeout) {
313
+ current.timeoutFailures += 1;
314
+ }
315
+ }
316
+ this.publishDebugState(state);
317
+ }
70
318
  isRetryableError(error) {
71
319
  // First check for non-retryable smart contract errors
72
320
  const errorMessage = error?.message?.toLowerCase() || '';
@@ -109,20 +357,141 @@ class RetryableProvider {
109
357
  errorCode.includes(retryableError) ||
110
358
  errorStatus.includes(retryableError));
111
359
  }
112
- async executeWithRetry(operation, context = 'RPC call') {
360
+ isReadFallbackActive(primaryState) {
361
+ if (this.primaryReadCooldownUntil === 0) {
362
+ return false;
363
+ }
364
+ if (Date.now() >= this.primaryReadCooldownUntil) {
365
+ this.primaryReadCooldownUntil = 0;
366
+ this._fallbackActivated = false;
367
+ if (primaryState) {
368
+ primaryState.cooldownUntil = 0;
369
+ this.publishDebugState(primaryState);
370
+ }
371
+ return false;
372
+ }
373
+ return true;
374
+ }
375
+ getFallbackProviderLabel(state) {
376
+ return state.label;
377
+ }
378
+ isProviderCoolingDown(cooldownUntil) {
379
+ return cooldownUntil > Date.now();
380
+ }
381
+ compareFallbackProviders(a, b) {
382
+ const aCooling = this.isProviderCoolingDown(a.cooldownUntil);
383
+ const bCooling = this.isProviderCoolingDown(b.cooldownUntil);
384
+ if (aCooling !== bCooling) {
385
+ return aCooling ? 1 : -1;
386
+ }
387
+ if (aCooling && bCooling) {
388
+ return a.cooldownUntil - b.cooldownUntil || a.index - b.index;
389
+ }
390
+ const aRankScore = this.getProviderRankScore(a);
391
+ const bRankScore = this.getProviderRankScore(b);
392
+ if (aRankScore !== bRankScore) {
393
+ return bRankScore - aRankScore;
394
+ }
395
+ const aSuccessRate = this.getProviderSuccessRate(a);
396
+ const bSuccessRate = this.getProviderSuccessRate(b);
397
+ if (aSuccessRate !== bSuccessRate) {
398
+ return bSuccessRate - aSuccessRate;
399
+ }
400
+ const aLatency = this.getProviderAverageLatency(a);
401
+ const bLatency = this.getProviderAverageLatency(b);
402
+ if (aLatency != null || bLatency != null) {
403
+ if (aLatency == null) {
404
+ return 1;
405
+ }
406
+ if (bLatency == null) {
407
+ return -1;
408
+ }
409
+ if (aLatency !== bLatency) {
410
+ return aLatency - bLatency;
411
+ }
412
+ }
413
+ if (a.lastSuccessAt !== b.lastSuccessAt) {
414
+ return b.lastSuccessAt - a.lastSuccessAt;
415
+ }
416
+ if (a.lastFailureAt !== b.lastFailureAt) {
417
+ return a.lastFailureAt - b.lastFailureAt;
418
+ }
419
+ return a.index - b.index;
420
+ }
421
+ getOrderedFallbackProviders() {
422
+ return [...this.fallbackProviderStates].sort((a, b) => this.compareFallbackProviders(a, b));
423
+ }
424
+ markFallbackFailure(state) {
425
+ state.lastFailureAt = Date.now();
426
+ state.cooldownUntil = state.lastFailureAt + this.config.fallbackCooldownMs;
427
+ this.publishDebugState(state);
428
+ }
429
+ markFallbackSuccess(state) {
430
+ state.lastSuccessAt = Date.now();
431
+ state.lastFailureAt = 0;
432
+ state.cooldownUntil = 0;
433
+ this.publishDebugState(state);
434
+ }
435
+ getOrderedFallbackOps(fallbackOps) {
436
+ const fallbackOpMap = new Map(fallbackOps.map((entry) => [entry.state, entry.operation]));
437
+ return this.getOrderedFallbackProviders()
438
+ .map((state) => {
439
+ const operation = fallbackOpMap.get(state);
440
+ if (!operation) {
441
+ return null;
442
+ }
443
+ return { state, operation };
444
+ })
445
+ .filter((entry) => entry != null);
446
+ }
447
+ async executeWithTimeout(operation, timeoutMs, context) {
448
+ if (timeoutMs <= 0) {
449
+ return operation();
450
+ }
451
+ let timeoutHandle = null;
452
+ try {
453
+ return await Promise.race([
454
+ operation(),
455
+ new Promise((_, reject) => {
456
+ timeoutHandle = setTimeout(() => {
457
+ const error = new Error(`[rpc] ${context}: timeout after ${timeoutMs}ms`);
458
+ error.code = "timeout";
459
+ reject(error);
460
+ }, timeoutMs);
461
+ }),
462
+ ]);
463
+ }
464
+ finally {
465
+ if (timeoutHandle != null) {
466
+ clearTimeout(timeoutHandle);
467
+ }
468
+ }
469
+ }
470
+ withReadTimeout(operation, context) {
471
+ return () => this.executeWithTimeout(operation, this.config.timeoutMs, context);
472
+ }
473
+ async executeWithRetry(operation, context = 'RPC call', providerState = null) {
113
474
  let lastError;
114
475
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
476
+ const startedAt = Date.now();
115
477
  try {
116
- return await operation();
478
+ const result = await operation();
479
+ if (providerState) {
480
+ this.recordProviderAttempt(providerState, Date.now() - startedAt);
481
+ }
482
+ return result;
117
483
  }
118
484
  catch (error) {
485
+ if (providerState) {
486
+ this.recordProviderAttempt(providerState, Date.now() - startedAt, error);
487
+ }
119
488
  lastError = error;
120
489
  const isRetryable = this.isRetryableError(error);
121
490
  // Don't retry on the last attempt or if error is not retryable
122
491
  if (attempt === this.config.maxRetries || !isRetryable) {
123
492
  if (!isRetryable && attempt === 0) {
124
493
  // Log that this error is not retryable for debugging
125
- console.debug(`${context} failed with non-retryable error: ${error.message}`);
494
+ console.debug(`[rpc] ${context}: non-retryable`, error.message);
126
495
  }
127
496
  throw error;
128
497
  }
@@ -131,17 +500,72 @@ class RetryableProvider {
131
500
  if (this.config.onRetry) {
132
501
  this.config.onRetry(attempt + 1, error, delay);
133
502
  }
134
- console.warn(`${context} failed (attempt ${attempt + 1}/${this.config.maxRetries + 1}): ${error.message}. Retrying in ${delay}ms...`);
503
+ console.debug(`[rpc] ${context}: attempt ${attempt + 1}/${this.config.maxRetries + 1} failed, retry in ${delay}ms`, error.message);
135
504
  await this.sleep(delay);
136
505
  }
137
506
  }
138
507
  throw lastError;
139
508
  }
509
+ /**
510
+ * Execute a read operation against the primary provider, falling back to
511
+ * the dedicated RPC providers if the primary exhausts all retries.
512
+ *
513
+ * Write operations (signing, sending transactions) never use the fallback
514
+ * because the fallback providers cannot sign.
515
+ */
516
+ async executeFallbackChain(fallbackOps, context, originalError) {
517
+ let lastError = originalError;
518
+ for (const { state, operation } of fallbackOps) {
519
+ const fallbackContext = `${context} [${this.getFallbackProviderLabel(state)}]`;
520
+ try {
521
+ this.recordProviderSelection(state);
522
+ const result = await this.executeWithRetry(this.withReadTimeout(operation, fallbackContext), fallbackContext, state);
523
+ this.markFallbackSuccess(state);
524
+ return result;
525
+ }
526
+ catch (fallbackError) {
527
+ if (!this.isRetryableError(fallbackError)) {
528
+ throw fallbackError;
529
+ }
530
+ this.markFallbackFailure(state);
531
+ lastError = fallbackError;
532
+ console.warn(`[rpc] ${fallbackContext} failed after ${this.config.maxRetries + 1} attempts. ` +
533
+ `Trying the next configured read RPC.`);
534
+ }
535
+ }
536
+ throw lastError;
537
+ }
538
+ async executeWithReadFallback(primaryOp, fallbackOps, context, primaryState) {
539
+ const timedPrimaryOp = this.withReadTimeout(primaryOp, context);
540
+ const orderedFallbackOps = this.getOrderedFallbackOps(fallbackOps);
541
+ if (this.isReadFallbackActive(primaryState)) {
542
+ return this.executeFallbackChain(orderedFallbackOps, context);
543
+ }
544
+ try {
545
+ return await this.executeWithRetry(timedPrimaryOp, context, primaryState);
546
+ }
547
+ catch (primaryError) {
548
+ if (!this.isRetryableError(primaryError)) {
549
+ throw primaryError;
550
+ }
551
+ this.primaryReadCooldownUntil = Date.now() + this.config.fallbackCooldownMs;
552
+ primaryState.cooldownUntil = this.primaryReadCooldownUntil;
553
+ this.publishDebugState(primaryState);
554
+ if (!this._fallbackActivated) {
555
+ this._fallbackActivated = true;
556
+ console.warn(`[rpc] Primary provider failed for ${context} after ${this.config.maxRetries + 1} attempts. ` +
557
+ `Falling back to dedicated RPCs for read operations for ${this.config.fallbackCooldownMs}ms.`);
558
+ }
559
+ return this.executeFallbackChain(orderedFallbackOps, context, primaryError);
560
+ }
561
+ }
140
562
  wrapProvider(provider) {
141
563
  // If it's already wrapped, return as-is
142
564
  if (provider._isRetryable) {
143
565
  return provider;
144
566
  }
567
+ const hasFallback = this.fallbackProviders.length > 0;
568
+ const primaryState = this.createProviderState(provider, 'primary', 'primary', -1);
145
569
  const retryableProvider = new Proxy(provider, {
146
570
  get: (target, prop, receiver) => {
147
571
  const original = Reflect.get(target, prop, receiver);
@@ -152,19 +576,54 @@ class RetryableProvider {
152
576
  // Wrap the main RPC send method
153
577
  if (prop === 'send' && typeof original === 'function') {
154
578
  return async (method, params) => {
155
- return this.executeWithRetry(() => original.apply(target, [method, params]), `RPC ${method}`);
579
+ const primaryOp = () => original.apply(target, [method, params]);
580
+ if (hasFallback) {
581
+ const fallbackOps = this.fallbackProviderStates.map((state) => ({
582
+ state,
583
+ operation: () => state.provider.send(method, params),
584
+ }));
585
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `RPC ${method}`, primaryState);
586
+ }
587
+ return this.executeWithRetry(primaryOp, `RPC ${method}`, primaryState);
156
588
  };
157
589
  }
158
590
  // For JsonRpcProvider, also wrap _send if it exists
159
591
  if (prop === '_send' && typeof original === 'function') {
160
592
  return async (payload, callback) => {
161
- return this.executeWithRetry(() => original.apply(target, arguments), `RPC ${payload.method || 'unknown'}`);
593
+ const method = payload.method || 'unknown';
594
+ const primaryOp = () => original.apply(target, [payload, callback]);
595
+ if (hasFallback) {
596
+ const fallbackOps = this.fallbackProviderStates.map((state) => ({
597
+ state,
598
+ operation: () => state.provider._send(payload, callback),
599
+ }));
600
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `RPC ${method}`, primaryState);
601
+ }
602
+ return this.executeWithRetry(primaryOp, `RPC ${method}`, primaryState);
162
603
  };
163
604
  }
164
605
  // Wrap other async methods that might make RPC calls
165
606
  if (typeof original === 'function' && this.isRpcMethod(prop)) {
166
607
  return async (...args) => {
167
- return this.executeWithRetry(() => original.apply(target, args), `Provider method ${String(prop)}`);
608
+ const primaryOp = () => original.apply(target, args);
609
+ if (hasFallback) {
610
+ const fallbackOps = this.fallbackProviderStates
611
+ .map((state) => {
612
+ const fbMethod = state.provider[prop];
613
+ if (typeof fbMethod !== 'function') {
614
+ return null;
615
+ }
616
+ return {
617
+ state,
618
+ operation: () => fbMethod.apply(state.provider, args),
619
+ };
620
+ })
621
+ .filter((entry) => entry != null);
622
+ if (fallbackOps.length > 0) {
623
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `Provider method ${String(prop)}`, primaryState);
624
+ }
625
+ }
626
+ return this.executeWithRetry(primaryOp, `Provider method ${String(prop)}`, primaryState);
168
627
  };
169
628
  }
170
629
  // If it's a function, bind it to the target
@@ -180,26 +639,7 @@ class RetryableProvider {
180
639
  if (typeof prop !== 'string')
181
640
  return false;
182
641
  // Common ethers.js methods that make RPC calls
183
- const rpcMethods = [
184
- 'getBalance',
185
- 'getCode',
186
- 'getStorageAt',
187
- 'getTransactionCount',
188
- 'getBlock',
189
- 'getBlockNumber',
190
- 'getGasPrice',
191
- 'getFeeData',
192
- 'getTransaction',
193
- 'getTransactionReceipt',
194
- 'call',
195
- 'estimateGas',
196
- 'sendTransaction',
197
- 'waitForTransaction',
198
- 'getLogs',
199
- 'getNetwork',
200
- 'detectNetwork'
201
- ];
202
- return rpcMethods.includes(prop);
642
+ return RPC_PROVIDER_METHODS.has(prop);
203
643
  }
204
644
  updateConfig(newConfig) {
205
645
  this.config = { ...this.config, ...newConfig };
@@ -225,8 +665,8 @@ function configureRetries(config = {}) {
225
665
  /**
226
666
  * Create a provider with retry capabilities
227
667
  */
228
- function createRetryableProvider(provider, config = {}) {
229
- const retryProvider = new RetryableProvider(config);
668
+ function createRetryableProvider(provider, config = {}, readFallback = null) {
669
+ const retryProvider = new RetryableProvider(config, readFallback);
230
670
  return retryProvider.wrapProvider(provider);
231
671
  }
232
672
  /**
@@ -239,11 +679,22 @@ function getGlobalRetryProvider() {
239
679
  return globalRetryProvider;
240
680
  }
241
681
  /**
242
- * Wrap a provider with the global retry configuration
682
+ * Wrap a provider with the global retry configuration.
683
+ *
684
+ * When `readFallback` is supplied, read-only RPC methods (eth_call,
685
+ * eth_getBalance, etc.) will fall through to the fallback providers after
686
+ * exhausting retries on the primary. Write/signing methods never use
687
+ * the fallback because only the primary (wallet) provider can sign.
243
688
  */
244
- function wrapProviderWithRetries(provider) {
245
- const retryProvider = getGlobalRetryProvider();
246
- return retryProvider.wrapProvider(provider);
689
+ function wrapProviderWithRetries(provider, readFallback = null) {
690
+ const hasFallback = Array.isArray(readFallback) ? readFallback.length > 0 : readFallback != null;
691
+ if (hasFallback) {
692
+ // Fallback is per-invocation — create a dedicated instance so the
693
+ // fallback providers aren't shared across setupChain calls.
694
+ const retryProvider = new RetryableProvider(getGlobalRetryProvider().getConfig(), readFallback);
695
+ return retryProvider.wrapProvider(provider);
696
+ }
697
+ return getGlobalRetryProvider().wrapProvider(provider);
247
698
  }
248
699
  /**
249
700
  * Utility function to check if a provider is already wrapped with retries
@@ -251,6 +702,9 @@ function wrapProviderWithRetries(provider) {
251
702
  function isRetryableProvider(provider) {
252
703
  return provider._isRetryable === true;
253
704
  }
705
+ function isRetryableReadProvider(provider) {
706
+ return provider._isRetryable === true;
707
+ }
254
708
  /**
255
709
  * Utility function to classify error types for debugging
256
710
  */