curvance 4.0.4 → 4.1.1

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 (125) hide show
  1. package/README.md +649 -59
  2. package/dist/chains/arb-sepolia.json +44 -0
  3. package/dist/chains/arbitrum.d.ts.map +1 -1
  4. package/dist/chains/arbitrum.js +4 -2
  5. package/dist/chains/arbitrum.js.map +1 -1
  6. package/dist/chains/index.d.ts +4 -0
  7. package/dist/chains/index.d.ts.map +1 -1
  8. package/dist/chains/index.js +15 -0
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/monad-mainnet.json +26 -1
  11. package/dist/chains/monad.d.ts.map +1 -1
  12. package/dist/chains/monad.js +4 -2
  13. package/dist/chains/monad.js.map +1 -1
  14. package/dist/chains/rpc.d.ts +57 -0
  15. package/dist/chains/rpc.d.ts.map +1 -0
  16. package/dist/chains/rpc.js +67 -0
  17. package/dist/chains/rpc.js.map +1 -0
  18. package/dist/classes/Api.d.ts +4 -3
  19. package/dist/classes/Api.d.ts.map +1 -1
  20. package/dist/classes/Api.js +7 -7
  21. package/dist/classes/Api.js.map +1 -1
  22. package/dist/classes/BorrowableCToken.d.ts +3 -2
  23. package/dist/classes/BorrowableCToken.d.ts.map +1 -1
  24. package/dist/classes/BorrowableCToken.js +6 -5
  25. package/dist/classes/BorrowableCToken.js.map +1 -1
  26. package/dist/classes/CToken.d.ts +11 -3
  27. package/dist/classes/CToken.d.ts.map +1 -1
  28. package/dist/classes/CToken.js +102 -108
  29. package/dist/classes/CToken.js.map +1 -1
  30. package/dist/classes/Calldata.d.ts +2 -2
  31. package/dist/classes/Calldata.d.ts.map +1 -1
  32. package/dist/classes/Calldata.js +2 -2
  33. package/dist/classes/Calldata.js.map +1 -1
  34. package/dist/classes/DexAggregators/IDexAgg.d.ts +2 -2
  35. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  36. package/dist/classes/DexAggregators/Kuru.d.ts +2 -2
  37. package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
  38. package/dist/classes/DexAggregators/Kuru.js +3 -4
  39. package/dist/classes/DexAggregators/Kuru.js.map +1 -1
  40. package/dist/classes/DexAggregators/KuruMainnet.d.ts +1 -0
  41. package/dist/classes/DexAggregators/KuruMainnet.d.ts.map +1 -0
  42. package/dist/classes/DexAggregators/KuruMainnet.js +228 -0
  43. package/dist/classes/DexAggregators/KuruMainnet.js.map +1 -0
  44. package/dist/classes/DexAggregators/KyberSwap.d.ts +2 -2
  45. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  46. package/dist/classes/DexAggregators/KyberSwap.js +22 -13
  47. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  48. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +2 -2
  49. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  50. package/dist/classes/DexAggregators/MultiDexAgg.js +3 -3
  51. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  52. package/dist/classes/ERC20.d.ts +5 -3
  53. package/dist/classes/ERC20.d.ts.map +1 -1
  54. package/dist/classes/ERC20.js +20 -14
  55. package/dist/classes/ERC20.js.map +1 -1
  56. package/dist/classes/ERC4626.d.ts.map +1 -1
  57. package/dist/classes/ERC4626.js +3 -1
  58. package/dist/classes/ERC4626.js.map +1 -1
  59. package/dist/classes/Kuru.d.ts +59 -0
  60. package/dist/classes/Kuru.d.ts.map +1 -0
  61. package/dist/classes/Kuru.js +167 -0
  62. package/dist/classes/Kuru.js.map +1 -0
  63. package/dist/classes/KuruMainnet.d.ts +59 -0
  64. package/dist/classes/KuruMainnet.d.ts.map +1 -0
  65. package/dist/classes/KuruMainnet.js +167 -0
  66. package/dist/classes/KuruMainnet.js.map +1 -0
  67. package/dist/classes/Market.d.ts +13 -4
  68. package/dist/classes/Market.d.ts.map +1 -1
  69. package/dist/classes/Market.js +87 -32
  70. package/dist/classes/Market.js.map +1 -1
  71. package/dist/classes/NativeToken.d.ts +6 -3
  72. package/dist/classes/NativeToken.d.ts.map +1 -1
  73. package/dist/classes/NativeToken.js +11 -16
  74. package/dist/classes/NativeToken.js.map +1 -1
  75. package/dist/classes/OptimizerReader.d.ts +3 -3
  76. package/dist/classes/OptimizerReader.d.ts.map +1 -1
  77. package/dist/classes/OptimizerReader.js +1 -1
  78. package/dist/classes/OptimizerReader.js.map +1 -1
  79. package/dist/classes/OracleManager.d.ts +3 -3
  80. package/dist/classes/OracleManager.d.ts.map +1 -1
  81. package/dist/classes/OracleManager.js +1 -1
  82. package/dist/classes/OracleManager.js.map +1 -1
  83. package/dist/classes/PositionManager.d.ts +2 -2
  84. package/dist/classes/PositionManager.d.ts.map +1 -1
  85. package/dist/classes/PositionManager.js +4 -4
  86. package/dist/classes/PositionManager.js.map +1 -1
  87. package/dist/classes/ProtocolReader.d.ts +23 -8
  88. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  89. package/dist/classes/ProtocolReader.js +197 -64
  90. package/dist/classes/ProtocolReader.js.map +1 -1
  91. package/dist/classes/Redstone.d.ts.map +1 -1
  92. package/dist/classes/Redstone.js +1 -2
  93. package/dist/classes/Redstone.js.map +1 -1
  94. package/dist/classes/Zapper.d.ts +4 -2
  95. package/dist/classes/Zapper.d.ts.map +1 -1
  96. package/dist/classes/Zapper.js +14 -13
  97. package/dist/classes/Zapper.js.map +1 -1
  98. package/dist/classes/index.d.ts +1 -1
  99. package/dist/classes/index.d.ts.map +1 -1
  100. package/dist/classes/index.js +6 -1
  101. package/dist/classes/index.js.map +1 -1
  102. package/dist/contracts/monad-mainnet.json +1 -1
  103. package/dist/helpers.d.ts +3 -1
  104. package/dist/helpers.d.ts.map +1 -1
  105. package/dist/helpers.js +34 -4
  106. package/dist/helpers.js.map +1 -1
  107. package/dist/integrations/snapshot.d.ts.map +1 -1
  108. package/dist/integrations/snapshot.js +4 -18
  109. package/dist/integrations/snapshot.js.map +1 -1
  110. package/dist/retry-provider.d.ts +83 -6
  111. package/dist/retry-provider.d.ts.map +1 -1
  112. package/dist/retry-provider.js +538 -68
  113. package/dist/retry-provider.js.map +1 -1
  114. package/dist/setup.d.ts +14 -3
  115. package/dist/setup.d.ts.map +1 -1
  116. package/dist/setup.js +67 -20
  117. package/dist/setup.js.map +1 -1
  118. package/dist/snapshot.d.ts +53 -0
  119. package/dist/snapshot.d.ts.map +1 -0
  120. package/dist/snapshot.js +103 -0
  121. package/dist/snapshot.js.map +1 -0
  122. package/dist/types.d.ts +2 -1
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js.map +1 -1
  125. package/package.json +5 -2
@@ -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,55 +155,343 @@ 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
  }
70
- isRetryableError(error) {
71
- // First check for non-retryable smart contract errors
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
+ }
318
+ // Contract / user-intent errors that are deterministic chain state. These
319
+ // must never be retried on the same provider AND must never cascade to a
320
+ // fallback provider — the next provider would return the same result.
321
+ static CONTRACT_ERROR_PATTERNS = [
322
+ 'revert',
323
+ 'execution reverted',
324
+ 'transaction reverted',
325
+ 'insufficient funds',
326
+ 'gas required exceeds allowance',
327
+ 'nonce too high',
328
+ 'nonce too low',
329
+ 'replacement transaction underpriced',
330
+ 'already pending',
331
+ 'invalid opcode',
332
+ 'stack overflow',
333
+ 'stack underflow',
334
+ 'out of gas',
335
+ 'call_exception',
336
+ 'unpredictable_gas_limit',
337
+ 'invalid_argument',
338
+ 'missing_argument',
339
+ 'unexpected_argument',
340
+ 'numeric_fault',
341
+ 'user rejected',
342
+ 'user denied',
343
+ 'user cancelled',
344
+ 'action_rejected',
345
+ '4001',
346
+ ];
347
+ isContractError(error) {
72
348
  const errorMessage = error?.message?.toLowerCase() || '';
73
349
  const errorCode = error?.code?.toString() || '';
74
- // These are contract execution errors that should NOT be retried
75
- const nonRetryablePatterns = [
76
- 'revert',
77
- 'execution reverted',
78
- 'transaction reverted',
79
- 'insufficient funds',
80
- 'gas required exceeds allowance',
81
- 'nonce too high',
82
- 'nonce too low',
83
- 'replacement transaction underpriced',
84
- 'already pending',
85
- 'invalid opcode',
86
- 'stack overflow',
87
- 'stack underflow',
88
- 'out of gas',
89
- 'call_exception',
90
- 'unpredictable_gas_limit',
91
- 'invalid_argument',
92
- 'missing_argument',
93
- 'unexpected_argument',
94
- 'numeric_fault',
95
- 'user rejected',
96
- 'user denied',
97
- 'user cancelled',
98
- 'action_rejected',
99
- '4001'
100
- ];
101
- // If it's a contract execution error, don't retry
102
- const isContractError = nonRetryablePatterns.some(pattern => errorMessage.includes(pattern) || errorCode.includes(pattern));
103
- if (isContractError) {
350
+ return RetryableProvider.CONTRACT_ERROR_PATTERNS.some((pattern) => errorMessage.includes(pattern) || errorCode.includes(pattern));
351
+ }
352
+ isRetryableError(error) {
353
+ // Retry policy — only known-transient errors warrant a same-provider
354
+ // retry. Unknown error shapes are NOT retried here (re-sending the
355
+ // same payload to the same broken endpoint rarely helps), but they
356
+ // DO still advance to the fallback chain via isContractError below.
357
+ if (this.isContractError(error)) {
104
358
  return false;
105
359
  }
106
- // Now check for retryable network/RPC errors
360
+ const errorMessage = error?.message?.toLowerCase() || '';
361
+ const errorCode = error?.code?.toString() || '';
107
362
  const errorStatus = error?.response?.status?.toString() || '';
108
363
  return this.config.retryableErrors.some(retryableError => errorMessage.includes(retryableError.toLowerCase()) ||
109
364
  errorCode.includes(retryableError) ||
110
365
  errorStatus.includes(retryableError));
111
366
  }
112
- async executeWithRetry(operation, context = 'RPC call') {
367
+ isReadFallbackActive(primaryState) {
368
+ if (this.primaryReadCooldownUntil === 0) {
369
+ return false;
370
+ }
371
+ if (Date.now() >= this.primaryReadCooldownUntil) {
372
+ this.primaryReadCooldownUntil = 0;
373
+ this._fallbackActivated = false;
374
+ if (primaryState) {
375
+ primaryState.cooldownUntil = 0;
376
+ this.publishDebugState(primaryState);
377
+ }
378
+ return false;
379
+ }
380
+ return true;
381
+ }
382
+ getFallbackProviderLabel(state) {
383
+ return state.label;
384
+ }
385
+ isProviderCoolingDown(cooldownUntil) {
386
+ return cooldownUntil > Date.now();
387
+ }
388
+ compareFallbackProviders(a, b) {
389
+ const aCooling = this.isProviderCoolingDown(a.cooldownUntil);
390
+ const bCooling = this.isProviderCoolingDown(b.cooldownUntil);
391
+ if (aCooling !== bCooling) {
392
+ return aCooling ? 1 : -1;
393
+ }
394
+ if (aCooling && bCooling) {
395
+ return a.cooldownUntil - b.cooldownUntil || a.index - b.index;
396
+ }
397
+ const aRankScore = this.getProviderRankScore(a);
398
+ const bRankScore = this.getProviderRankScore(b);
399
+ if (aRankScore !== bRankScore) {
400
+ return bRankScore - aRankScore;
401
+ }
402
+ const aSuccessRate = this.getProviderSuccessRate(a);
403
+ const bSuccessRate = this.getProviderSuccessRate(b);
404
+ if (aSuccessRate !== bSuccessRate) {
405
+ return bSuccessRate - aSuccessRate;
406
+ }
407
+ const aLatency = this.getProviderAverageLatency(a);
408
+ const bLatency = this.getProviderAverageLatency(b);
409
+ if (aLatency != null || bLatency != null) {
410
+ if (aLatency == null) {
411
+ return 1;
412
+ }
413
+ if (bLatency == null) {
414
+ return -1;
415
+ }
416
+ if (aLatency !== bLatency) {
417
+ return aLatency - bLatency;
418
+ }
419
+ }
420
+ if (a.lastSuccessAt !== b.lastSuccessAt) {
421
+ return b.lastSuccessAt - a.lastSuccessAt;
422
+ }
423
+ if (a.lastFailureAt !== b.lastFailureAt) {
424
+ return a.lastFailureAt - b.lastFailureAt;
425
+ }
426
+ return a.index - b.index;
427
+ }
428
+ getOrderedFallbackProviders() {
429
+ return [...this.fallbackProviderStates].sort((a, b) => this.compareFallbackProviders(a, b));
430
+ }
431
+ markFallbackFailure(state) {
432
+ state.lastFailureAt = Date.now();
433
+ state.cooldownUntil = state.lastFailureAt + this.config.fallbackCooldownMs;
434
+ this.publishDebugState(state);
435
+ }
436
+ markFallbackSuccess(state) {
437
+ state.lastSuccessAt = Date.now();
438
+ state.lastFailureAt = 0;
439
+ state.cooldownUntil = 0;
440
+ this.publishDebugState(state);
441
+ }
442
+ getOrderedFallbackOps(fallbackOps) {
443
+ const fallbackOpMap = new Map(fallbackOps.map((entry) => [entry.state, entry.operation]));
444
+ return this.getOrderedFallbackProviders()
445
+ .map((state) => {
446
+ const operation = fallbackOpMap.get(state);
447
+ if (!operation) {
448
+ return null;
449
+ }
450
+ return { state, operation };
451
+ })
452
+ .filter((entry) => entry != null);
453
+ }
454
+ async executeWithTimeout(operation, timeoutMs, context) {
455
+ if (timeoutMs <= 0) {
456
+ return operation();
457
+ }
458
+ let timeoutHandle = null;
459
+ try {
460
+ return await Promise.race([
461
+ operation(),
462
+ new Promise((_, reject) => {
463
+ timeoutHandle = setTimeout(() => {
464
+ const error = new Error(`[rpc] ${context}: timeout after ${timeoutMs}ms`);
465
+ error.code = "timeout";
466
+ reject(error);
467
+ }, timeoutMs);
468
+ }),
469
+ ]);
470
+ }
471
+ finally {
472
+ if (timeoutHandle != null) {
473
+ clearTimeout(timeoutHandle);
474
+ }
475
+ }
476
+ }
477
+ withReadTimeout(operation, context) {
478
+ return () => this.executeWithTimeout(operation, this.config.timeoutMs, context);
479
+ }
480
+ async executeWithRetry(operation, context = 'RPC call', providerState = null) {
113
481
  let lastError;
114
482
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
483
+ const startedAt = Date.now();
115
484
  try {
116
- return await operation();
485
+ const result = await operation();
486
+ if (providerState) {
487
+ this.recordProviderAttempt(providerState, Date.now() - startedAt);
488
+ }
489
+ return result;
117
490
  }
118
491
  catch (error) {
492
+ if (providerState) {
493
+ this.recordProviderAttempt(providerState, Date.now() - startedAt, error);
494
+ }
119
495
  lastError = error;
120
496
  const isRetryable = this.isRetryableError(error);
121
497
  // Don't retry on the last attempt or if error is not retryable
@@ -137,11 +513,75 @@ class RetryableProvider {
137
513
  }
138
514
  throw lastError;
139
515
  }
516
+ /**
517
+ * Execute a read operation against the primary provider, falling back to
518
+ * the dedicated RPC providers if the primary exhausts all retries.
519
+ *
520
+ * Write operations (signing, sending transactions) never use the fallback
521
+ * because the fallback providers cannot sign.
522
+ */
523
+ async executeFallbackChain(fallbackOps, context, originalError) {
524
+ let lastError = originalError;
525
+ for (const { state, operation } of fallbackOps) {
526
+ const fallbackContext = `${context} [${this.getFallbackProviderLabel(state)}]`;
527
+ try {
528
+ this.recordProviderSelection(state);
529
+ const result = await this.executeWithRetry(this.withReadTimeout(operation, fallbackContext), fallbackContext, state);
530
+ this.markFallbackSuccess(state);
531
+ return result;
532
+ }
533
+ catch (fallbackError) {
534
+ // Only deterministic contract/user errors short-circuit the
535
+ // fallback chain. Unknown transport-level errors on one
536
+ // fallback must still advance to the next configured provider
537
+ // — a malformed response or auth issue on fallback N is not
538
+ // a reason to abandon fallback N+1.
539
+ if (this.isContractError(fallbackError)) {
540
+ throw fallbackError;
541
+ }
542
+ this.markFallbackFailure(state);
543
+ lastError = fallbackError;
544
+ console.warn(`[rpc] ${fallbackContext} failed after ${this.config.maxRetries + 1} attempts. ` +
545
+ `Trying the next configured read RPC.`);
546
+ }
547
+ }
548
+ throw lastError;
549
+ }
550
+ async executeWithReadFallback(primaryOp, fallbackOps, context, primaryState) {
551
+ const timedPrimaryOp = this.withReadTimeout(primaryOp, context);
552
+ const orderedFallbackOps = this.getOrderedFallbackOps(fallbackOps);
553
+ if (this.isReadFallbackActive(primaryState)) {
554
+ return this.executeFallbackChain(orderedFallbackOps, context);
555
+ }
556
+ try {
557
+ return await this.executeWithRetry(timedPrimaryOp, context, primaryState);
558
+ }
559
+ catch (primaryError) {
560
+ // Contract/user errors skip fallback — deterministic chain state.
561
+ // Everything else (known-retryable AND unknown transport errors)
562
+ // advances to the fallback chain so a broken primary can't block
563
+ // reads while healthy fallbacks sit idle.
564
+ if (this.isContractError(primaryError)) {
565
+ throw primaryError;
566
+ }
567
+ this.primaryReadCooldownUntil = Date.now() + this.config.fallbackCooldownMs;
568
+ primaryState.cooldownUntil = this.primaryReadCooldownUntil;
569
+ this.publishDebugState(primaryState);
570
+ if (!this._fallbackActivated) {
571
+ this._fallbackActivated = true;
572
+ console.warn(`[rpc] Primary provider failed for ${context} after ${this.config.maxRetries + 1} attempts. ` +
573
+ `Falling back to dedicated RPCs for read operations for ${this.config.fallbackCooldownMs}ms.`);
574
+ }
575
+ return this.executeFallbackChain(orderedFallbackOps, context, primaryError);
576
+ }
577
+ }
140
578
  wrapProvider(provider) {
141
579
  // If it's already wrapped, return as-is
142
580
  if (provider._isRetryable) {
143
581
  return provider;
144
582
  }
583
+ const hasFallback = this.fallbackProviders.length > 0;
584
+ const primaryState = this.createProviderState(provider, 'primary', 'primary', -1);
145
585
  const retryableProvider = new Proxy(provider, {
146
586
  get: (target, prop, receiver) => {
147
587
  const original = Reflect.get(target, prop, receiver);
@@ -152,19 +592,54 @@ class RetryableProvider {
152
592
  // Wrap the main RPC send method
153
593
  if (prop === 'send' && typeof original === 'function') {
154
594
  return async (method, params) => {
155
- return this.executeWithRetry(() => original.apply(target, [method, params]), `RPC ${method}`);
595
+ const primaryOp = () => original.apply(target, [method, params]);
596
+ if (hasFallback) {
597
+ const fallbackOps = this.fallbackProviderStates.map((state) => ({
598
+ state,
599
+ operation: () => state.provider.send(method, params),
600
+ }));
601
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `RPC ${method}`, primaryState);
602
+ }
603
+ return this.executeWithRetry(primaryOp, `RPC ${method}`, primaryState);
156
604
  };
157
605
  }
158
606
  // For JsonRpcProvider, also wrap _send if it exists
159
607
  if (prop === '_send' && typeof original === 'function') {
160
608
  return async (payload, callback) => {
161
- return this.executeWithRetry(() => original.apply(target, [payload, callback]), `RPC ${payload.method || 'unknown'}`);
609
+ const method = payload.method || 'unknown';
610
+ const primaryOp = () => original.apply(target, [payload, callback]);
611
+ if (hasFallback) {
612
+ const fallbackOps = this.fallbackProviderStates.map((state) => ({
613
+ state,
614
+ operation: () => state.provider._send(payload, callback),
615
+ }));
616
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `RPC ${method}`, primaryState);
617
+ }
618
+ return this.executeWithRetry(primaryOp, `RPC ${method}`, primaryState);
162
619
  };
163
620
  }
164
621
  // Wrap other async methods that might make RPC calls
165
622
  if (typeof original === 'function' && this.isRpcMethod(prop)) {
166
623
  return async (...args) => {
167
- return this.executeWithRetry(() => original.apply(target, args), `Provider method ${String(prop)}`);
624
+ const primaryOp = () => original.apply(target, args);
625
+ if (hasFallback) {
626
+ const fallbackOps = this.fallbackProviderStates
627
+ .map((state) => {
628
+ const fbMethod = state.provider[prop];
629
+ if (typeof fbMethod !== 'function') {
630
+ return null;
631
+ }
632
+ return {
633
+ state,
634
+ operation: () => fbMethod.apply(state.provider, args),
635
+ };
636
+ })
637
+ .filter((entry) => entry != null);
638
+ if (fallbackOps.length > 0) {
639
+ return this.executeWithReadFallback(primaryOp, fallbackOps, `Provider method ${String(prop)}`, primaryState);
640
+ }
641
+ }
642
+ return this.executeWithRetry(primaryOp, `Provider method ${String(prop)}`, primaryState);
168
643
  };
169
644
  }
170
645
  // If it's a function, bind it to the target
@@ -180,26 +655,7 @@ class RetryableProvider {
180
655
  if (typeof prop !== 'string')
181
656
  return false;
182
657
  // 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);
658
+ return RPC_PROVIDER_METHODS.has(prop);
203
659
  }
204
660
  updateConfig(newConfig) {
205
661
  this.config = { ...this.config, ...newConfig };
@@ -225,8 +681,8 @@ function configureRetries(config = {}) {
225
681
  /**
226
682
  * Create a provider with retry capabilities
227
683
  */
228
- function createRetryableProvider(provider, config = {}) {
229
- const retryProvider = new RetryableProvider(config);
684
+ function createRetryableProvider(provider, config = {}, readFallback = null) {
685
+ const retryProvider = new RetryableProvider(config, readFallback);
230
686
  return retryProvider.wrapProvider(provider);
231
687
  }
232
688
  /**
@@ -239,11 +695,22 @@ function getGlobalRetryProvider() {
239
695
  return globalRetryProvider;
240
696
  }
241
697
  /**
242
- * Wrap a provider with the global retry configuration
698
+ * Wrap a provider with the global retry configuration.
699
+ *
700
+ * When `readFallback` is supplied, read-only RPC methods (eth_call,
701
+ * eth_getBalance, etc.) will fall through to the fallback providers after
702
+ * exhausting retries on the primary. Write/signing methods never use
703
+ * the fallback because only the primary (wallet) provider can sign.
243
704
  */
244
- function wrapProviderWithRetries(provider) {
245
- const retryProvider = getGlobalRetryProvider();
246
- return retryProvider.wrapProvider(provider);
705
+ function wrapProviderWithRetries(provider, readFallback = null) {
706
+ const hasFallback = Array.isArray(readFallback) ? readFallback.length > 0 : readFallback != null;
707
+ if (hasFallback) {
708
+ // Fallback is per-invocation — create a dedicated instance so the
709
+ // fallback providers aren't shared across setupChain calls.
710
+ const retryProvider = new RetryableProvider(getGlobalRetryProvider().getConfig(), readFallback);
711
+ return retryProvider.wrapProvider(provider);
712
+ }
713
+ return getGlobalRetryProvider().wrapProvider(provider);
247
714
  }
248
715
  /**
249
716
  * Utility function to check if a provider is already wrapped with retries
@@ -251,6 +718,9 @@ function wrapProviderWithRetries(provider) {
251
718
  function isRetryableProvider(provider) {
252
719
  return provider._isRetryable === true;
253
720
  }
721
+ function isRetryableReadProvider(provider) {
722
+ return provider._isRetryable === true;
723
+ }
254
724
  /**
255
725
  * Utility function to classify error types for debugging
256
726
  */