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.
- package/README.md +595 -59
- package/dist/chains/arbitrum.d.ts.map +1 -1
- package/dist/chains/arbitrum.js +4 -2
- package/dist/chains/arbitrum.js.map +1 -1
- package/dist/chains/index.d.ts +4 -0
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js +15 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/monad.d.ts.map +1 -1
- package/dist/chains/monad.js +4 -2
- package/dist/chains/monad.js.map +1 -1
- package/dist/chains/rpc.d.ts +57 -0
- package/dist/chains/rpc.d.ts.map +1 -0
- package/dist/chains/rpc.js +47 -0
- package/dist/chains/rpc.js.map +1 -0
- package/dist/classes/Api.d.ts +4 -3
- package/dist/classes/Api.d.ts.map +1 -1
- package/dist/classes/Api.js +7 -7
- package/dist/classes/Api.js.map +1 -1
- package/dist/classes/BorrowableCToken.d.ts +3 -2
- package/dist/classes/BorrowableCToken.d.ts.map +1 -1
- package/dist/classes/BorrowableCToken.js +6 -5
- package/dist/classes/BorrowableCToken.js.map +1 -1
- package/dist/classes/CToken.d.ts +11 -3
- package/dist/classes/CToken.d.ts.map +1 -1
- package/dist/classes/CToken.js +168 -116
- package/dist/classes/CToken.js.map +1 -1
- package/dist/classes/Calldata.d.ts +2 -2
- package/dist/classes/Calldata.d.ts.map +1 -1
- package/dist/classes/Calldata.js +2 -2
- package/dist/classes/Calldata.js.map +1 -1
- package/dist/classes/DexAggregators/IDexAgg.d.ts +2 -2
- package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/Kuru.d.ts +2 -2
- package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
- package/dist/classes/DexAggregators/Kuru.js +3 -4
- package/dist/classes/DexAggregators/Kuru.js.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.d.ts +2 -2
- package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.js +88 -9
- package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts +2 -2
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.js +3 -3
- package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
- package/dist/classes/ERC20.d.ts +5 -3
- package/dist/classes/ERC20.d.ts.map +1 -1
- package/dist/classes/ERC20.js +20 -14
- package/dist/classes/ERC20.js.map +1 -1
- package/dist/classes/ERC4626.d.ts.map +1 -1
- package/dist/classes/ERC4626.js +3 -1
- package/dist/classes/ERC4626.js.map +1 -1
- package/dist/classes/Market.d.ts +13 -4
- package/dist/classes/Market.d.ts.map +1 -1
- package/dist/classes/Market.js +86 -28
- package/dist/classes/Market.js.map +1 -1
- package/dist/classes/NativeToken.d.ts +6 -3
- package/dist/classes/NativeToken.d.ts.map +1 -1
- package/dist/classes/NativeToken.js +11 -16
- package/dist/classes/NativeToken.js.map +1 -1
- package/dist/classes/OptimizerReader.d.ts +3 -3
- package/dist/classes/OptimizerReader.d.ts.map +1 -1
- package/dist/classes/OptimizerReader.js +1 -1
- package/dist/classes/OptimizerReader.js.map +1 -1
- package/dist/classes/OracleManager.d.ts +3 -3
- package/dist/classes/OracleManager.d.ts.map +1 -1
- package/dist/classes/OracleManager.js +1 -1
- package/dist/classes/OracleManager.js.map +1 -1
- package/dist/classes/PositionManager.d.ts +2 -2
- package/dist/classes/PositionManager.d.ts.map +1 -1
- package/dist/classes/PositionManager.js +4 -4
- package/dist/classes/PositionManager.js.map +1 -1
- package/dist/classes/ProtocolReader.d.ts +18 -4
- package/dist/classes/ProtocolReader.d.ts.map +1 -1
- package/dist/classes/ProtocolReader.js +177 -55
- package/dist/classes/ProtocolReader.js.map +1 -1
- package/dist/classes/Redstone.d.ts.map +1 -1
- package/dist/classes/Redstone.js +1 -2
- package/dist/classes/Redstone.js.map +1 -1
- package/dist/classes/Zapper.d.ts +4 -2
- package/dist/classes/Zapper.d.ts.map +1 -1
- package/dist/classes/Zapper.js +16 -14
- package/dist/classes/Zapper.js.map +1 -1
- package/dist/classes/index.d.ts +1 -1
- package/dist/classes/index.d.ts.map +1 -1
- package/dist/classes/index.js +6 -1
- package/dist/classes/index.js.map +1 -1
- package/dist/contracts/monad-mainnet.json +1 -1
- package/dist/feePolicy.d.ts +27 -1
- package/dist/feePolicy.d.ts.map +1 -1
- package/dist/feePolicy.js +10 -2
- package/dist/feePolicy.js.map +1 -1
- package/dist/helpers.d.ts +3 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +34 -4
- package/dist/helpers.js.map +1 -1
- package/dist/integrations/snapshot.d.ts.map +1 -1
- package/dist/integrations/snapshot.js +4 -18
- package/dist/integrations/snapshot.js.map +1 -1
- package/dist/retry-provider.d.ts +81 -6
- package/dist/retry-provider.d.ts.map +1 -1
- package/dist/retry-provider.js +491 -37
- package/dist/retry-provider.js.map +1 -1
- package/dist/setup.d.ts +14 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +56 -20
- package/dist/setup.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
package/dist/retry-provider.js
CHANGED
|
@@ -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:
|
|
11
|
-
baseDelay:
|
|
12
|
-
maxDelay:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
246
|
-
|
|
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
|
*/
|