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.
- package/README.md +649 -59
- package/dist/chains/arb-sepolia.json +44 -0
- 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-mainnet.json +26 -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 +67 -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 +102 -108
- 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/KuruMainnet.d.ts +1 -0
- package/dist/classes/DexAggregators/KuruMainnet.d.ts.map +1 -0
- package/dist/classes/DexAggregators/KuruMainnet.js +228 -0
- package/dist/classes/DexAggregators/KuruMainnet.js.map +1 -0
- 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 +22 -13
- 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/Kuru.d.ts +59 -0
- package/dist/classes/Kuru.d.ts.map +1 -0
- package/dist/classes/Kuru.js +167 -0
- package/dist/classes/Kuru.js.map +1 -0
- package/dist/classes/KuruMainnet.d.ts +59 -0
- package/dist/classes/KuruMainnet.d.ts.map +1 -0
- package/dist/classes/KuruMainnet.js +167 -0
- package/dist/classes/KuruMainnet.js.map +1 -0
- package/dist/classes/Market.d.ts +13 -4
- package/dist/classes/Market.d.ts.map +1 -1
- package/dist/classes/Market.js +87 -32
- 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 +23 -8
- package/dist/classes/ProtocolReader.d.ts.map +1 -1
- package/dist/classes/ProtocolReader.js +197 -64
- 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 +14 -13
- 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/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 +83 -6
- package/dist/retry-provider.d.ts.map +1 -1
- package/dist/retry-provider.js +538 -68
- 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 +67 -20
- package/dist/setup.js.map +1 -1
- package/dist/snapshot.d.ts +53 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +103 -0
- package/dist/snapshot.js.map +1 -0
- 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 +5 -2
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,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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
246
|
-
|
|
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
|
*/
|