@lido-nestjs/execution 1.2.0 → 1.5.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.
@@ -0,0 +1,5 @@
1
+ export declare const nonRetryableErrors: (string | number)[];
2
+ export declare type ErrorWithCode = Error & {
3
+ code: number | string;
4
+ };
5
+ export declare const isErrorHasCode: (error: unknown) => error is ErrorWithCode;
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var logger = require('@ethersproject/logger');
6
+
7
+ const nonRetryableErrors = [
8
+ ///////////////////
9
+ // Generic Errors
10
+ // Not Implemented
11
+ logger.ErrorCode.NOT_IMPLEMENTED,
12
+ // Timeout
13
+ logger.ErrorCode.TIMEOUT,
14
+ ///////////////////
15
+ // Operational Errors
16
+ // Buffer Overrun
17
+ logger.ErrorCode.BUFFER_OVERRUN,
18
+ // Numeric Fault
19
+ // - operation: the operation being executed
20
+ // - fault: the reason this faulted
21
+ logger.ErrorCode.NUMERIC_FAULT,
22
+ ///////////////////
23
+ // Argument Errors
24
+ // Missing new operator to an object
25
+ // - name: The name of the class
26
+ logger.ErrorCode.MISSING_NEW,
27
+ // Invalid argument (e.g. value is incompatible with type) to a function:
28
+ // - argument: The argument name that was invalid
29
+ // - value: The value of the argument
30
+ logger.ErrorCode.INVALID_ARGUMENT,
31
+ // Missing argument to a function:
32
+ // - count: The number of arguments received
33
+ // - expectedCount: The number of arguments expected
34
+ logger.ErrorCode.MISSING_ARGUMENT,
35
+ // Too many arguments
36
+ // - count: The number of arguments received
37
+ // - expectedCount: The number of arguments expected
38
+ logger.ErrorCode.UNEXPECTED_ARGUMENT,
39
+ ///////////////////
40
+ // Blockchain Errors
41
+ // Call exception
42
+ // - transaction: the transaction
43
+ // - address?: the contract address
44
+ // - args?: The arguments passed into the function
45
+ // - method?: The Solidity method signature
46
+ // - errorSignature?: The EIP848 error signature
47
+ // - errorArgs?: The EIP848 error parameters
48
+ // - reason: The reason (only for EIP848 "Error(string)")
49
+ logger.ErrorCode.CALL_EXCEPTION,
50
+ // Insufficient funds (< value + gasLimit * gasPrice)
51
+ // - transaction: the transaction attempted
52
+ logger.ErrorCode.INSUFFICIENT_FUNDS,
53
+ // Nonce has already been used
54
+ // - transaction: the transaction attempted
55
+ logger.ErrorCode.NONCE_EXPIRED,
56
+ // The replacement fee for the transaction is too low
57
+ // - transaction: the transaction attempted
58
+ logger.ErrorCode.REPLACEMENT_UNDERPRICED,
59
+ // The gas limit could not be estimated
60
+ // - transaction: the transaction passed to estimateGas
61
+ logger.ErrorCode.UNPREDICTABLE_GAS_LIMIT,
62
+ // The transaction was replaced by one with a higher gas price
63
+ // - reason: "cancelled", "replaced" or "repriced"
64
+ // - cancelled: true if reason == "cancelled" or reason == "replaced")
65
+ // - hash: original transaction hash
66
+ // - replacement: the full TransactionsResponse for the replacement
67
+ // - receipt: the receipt of the replacement
68
+ logger.ErrorCode.TRANSACTION_REPLACED,
69
+ ];
70
+ const isErrorHasCode = (error) => {
71
+ return (error instanceof Error &&
72
+ Object.prototype.hasOwnProperty.call(error, 'code'));
73
+ };
74
+
75
+ exports.isErrorHasCode = isErrorHasCode;
76
+ exports.nonRetryableErrors = nonRetryableErrors;
@@ -1,2 +1,2 @@
1
1
  import { LoggerService } from '@nestjs/common/services/logger.service';
2
- export declare const retrier: (logger?: LoggerService | null | undefined, defaultMaxRetryCount?: number, defaultMinBackoffMs?: number, defaultMaxBackoffMs?: number, defaultLogWarning?: boolean) => <T extends unknown>(callback: () => T | Promise<T>, maxRetryCount?: number | undefined, minBackoffMs?: number | undefined, maxBackoffMs?: number | undefined, logWarning?: boolean | undefined) => Promise<T>;
2
+ export declare const retrier: (logger?: LoggerService | null | undefined, defaultMaxRetryCount?: number, defaultMinBackoffMs?: number, defaultMaxBackoffMs?: number, defaultLogWarning?: boolean, defaultErrorFilter?: ((error: Error | unknown) => boolean) | undefined) => <T extends unknown>(callback: () => T | Promise<T>, maxRetryCount?: number | undefined, minBackoffMs?: number | undefined, maxBackoffMs?: number | undefined, logWarning?: boolean | undefined, errorFilter?: ((error: Error | unknown) => boolean) | undefined) => Promise<T>;
@@ -4,16 +4,20 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var sleep = require('./sleep.js');
6
6
 
7
- const retrier = (logger, defaultMaxRetryCount = 3, defaultMinBackoffMs = 1000, defaultMaxBackoffMs = 60000, defaultLogWarning = false) => {
8
- return async (callback, maxRetryCount, minBackoffMs, maxBackoffMs, logWarning) => {
7
+ const retrier = (logger, defaultMaxRetryCount = 3, defaultMinBackoffMs = 1000, defaultMaxBackoffMs = 60000, defaultLogWarning = false, defaultErrorFilter) => {
8
+ return async (callback, maxRetryCount, minBackoffMs, maxBackoffMs, logWarning, errorFilter) => {
9
9
  maxRetryCount = maxRetryCount !== null && maxRetryCount !== void 0 ? maxRetryCount : defaultMaxRetryCount;
10
10
  minBackoffMs = minBackoffMs !== null && minBackoffMs !== void 0 ? minBackoffMs : defaultMinBackoffMs;
11
11
  maxBackoffMs = maxBackoffMs !== null && maxBackoffMs !== void 0 ? maxBackoffMs : defaultMaxBackoffMs;
12
12
  logWarning = logWarning !== null && logWarning !== void 0 ? logWarning : defaultLogWarning;
13
+ errorFilter = errorFilter !== null && errorFilter !== void 0 ? errorFilter : defaultErrorFilter;
13
14
  try {
14
15
  return await callback();
15
16
  }
16
17
  catch (err) {
18
+ if (typeof errorFilter === 'function' && errorFilter(err)) {
19
+ throw err;
20
+ }
17
21
  if (logger && logWarning) {
18
22
  logger.warn(err, 'Retrying after (%dms). Remaining retries [%d]', minBackoffMs, maxRetryCount);
19
23
  }
@@ -21,7 +25,7 @@ const retrier = (logger, defaultMaxRetryCount = 3, defaultMinBackoffMs = 1000, d
21
25
  throw err;
22
26
  }
23
27
  await sleep.sleep(minBackoffMs);
24
- return await retrier(logger)(callback, maxRetryCount - 1, minBackoffMs * 2, maxBackoffMs, logWarning);
28
+ return await retrier(logger)(callback, maxRetryCount - 1, minBackoffMs * 2, maxBackoffMs, logWarning, errorFilter);
25
29
  }
26
30
  };
27
31
  };
@@ -0,0 +1,7 @@
1
+ export declare class AllProvidersFailedError extends Error {
2
+ name: string;
3
+ message: string;
4
+ code: number;
5
+ originalError: Error | unknown;
6
+ constructor(message: string);
7
+ }
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ class AllProvidersFailedError extends Error {
6
+ constructor(message) {
7
+ super('');
8
+ this.name = 'AllProvidersFailedError';
9
+ this.code = 0;
10
+ this.message = message;
11
+ }
12
+ }
13
+
14
+ exports.AllProvidersFailedError = AllProvidersFailedError;
@@ -0,0 +1,6 @@
1
+ export declare class NoNewBlocksWhilePollingError extends Error {
2
+ name: string;
3
+ message: string;
4
+ latestObservedBlockNumber: number;
5
+ constructor(message: string, latestObservedBlockNumber: number);
6
+ }
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ class NoNewBlocksWhilePollingError extends Error {
6
+ constructor(message, latestObservedBlockNumber) {
7
+ super('');
8
+ this.name = 'NoNewBlocksWhilePollingError';
9
+ this.message = message;
10
+ this.latestObservedBlockNumber = latestObservedBlockNumber;
11
+ }
12
+ }
13
+
14
+ exports.NoNewBlocksWhilePollingError = NoNewBlocksWhilePollingError;
@@ -4,4 +4,5 @@ export interface FallbackProvider {
4
4
  provider: ExtendedJsonRpcBatchProvider;
5
5
  network: Network | null;
6
6
  index: number;
7
+ unreachable: boolean;
7
8
  }
@@ -11,5 +11,7 @@ export interface SimpleFallbackProviderConfig {
11
11
  minBackoffMs?: number;
12
12
  maxBackoffMs?: number;
13
13
  logRetries?: boolean;
14
+ resetIntervalMs?: number;
14
15
  fetchMiddlewares?: MiddlewareCallback<Promise<any>>[];
16
+ maxTimeWithoutNewBlocksMs?: number;
15
17
  }
@@ -1,4 +1,5 @@
1
1
  import { BaseProvider, Formatter } from '@ethersproject/providers';
2
+ import { CallOverrides as CallOverridesSource } from '@ethersproject/contracts';
2
3
  import { SimpleFallbackProviderConfig } from '../interfaces/simple-fallback-provider-config';
3
4
  import { Network } from '@ethersproject/networks';
4
5
  import { LoggerService } from '@nestjs/common';
@@ -7,6 +8,7 @@ import { BlockTag } from '../ethers/block-tag';
7
8
  import { BigNumber, BigNumberish } from '@ethersproject/bignumber';
8
9
  import { Deferrable } from '@ethersproject/properties';
9
10
  import { TransactionRequest } from '@ethersproject/abstract-provider/src.ts/index';
11
+ import { EventType, Listener } from '@ethersproject/abstract-provider';
10
12
  /**
11
13
  * EIP-1898 support
12
14
  * https://eips.ethereum.org/EIPS/eip-1898
@@ -19,6 +21,9 @@ declare module '@ethersproject/providers' {
19
21
  getStorageAt(addressOrName: string | Promise<string>, position: BigNumberish | Promise<BigNumberish>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>;
20
22
  call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string>;
21
23
  }
24
+ interface CallOverrides extends Omit<CallOverridesSource, 'blockTag'> {
25
+ blockTag?: BlockTag;
26
+ }
22
27
  }
23
28
  export declare class SimpleFallbackJsonRpcBatchProvider extends BaseProvider {
24
29
  protected config: SimpleFallbackProviderConfig;
@@ -26,14 +31,19 @@ export declare class SimpleFallbackJsonRpcBatchProvider extends BaseProvider {
26
31
  protected fallbackProviders: [FallbackProvider];
27
32
  protected activeFallbackProviderIndex: number;
28
33
  protected detectNetworkFirstRun: boolean;
34
+ protected resetTimer: ReturnType<typeof setTimeout> | null;
35
+ protected lastPerformError: Error | null | unknown;
29
36
  constructor(config: SimpleFallbackProviderConfig, logger: LoggerService);
30
37
  static _formatter: Formatter | null;
31
38
  static getFormatter(): Formatter;
39
+ on(eventName: EventType, listener: Listener): this;
32
40
  protected get provider(): FallbackProvider;
33
41
  protected switchToNextProvider(): void;
42
+ protected errorShouldBeReThrown(error: Error | unknown): boolean;
34
43
  perform(method: string, params: {
35
44
  [name: string]: unknown;
36
45
  }): Promise<unknown>;
37
46
  detectNetwork(): Promise<Network>;
47
+ protected resetFallbacks(): void;
38
48
  protected networksEqual(networkA: Network, networkB: Network): boolean;
39
49
  }
@@ -9,12 +9,17 @@ var common = require('@nestjs/common');
9
9
  var retrier = require('../common/retrier.js');
10
10
  var formatterWithEip1898 = require('../ethers/formatter-with-eip1898.js');
11
11
  var networks = require('../common/networks.js');
12
+ var noNewBlocksWhilePolling_error = require('../error/no-new-blocks-while-polling.error.js');
13
+ var errors = require('../common/errors.js');
14
+ var allProvidersFailed_error = require('../error/all-providers-failed.error.js');
12
15
 
13
16
  exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchProvider extends providers.BaseProvider {
14
17
  constructor(config, logger) {
15
18
  super(config.network);
16
19
  this.detectNetworkFirstRun = true;
17
- this.config = Object.assign({ maxRetries: 3, minBackoffMs: 500, maxBackoffMs: 5000, logRetries: true }, config);
20
+ this.resetTimer = null;
21
+ this.lastPerformError = null;
22
+ this.config = Object.assign({ maxRetries: 3, minBackoffMs: 500, maxBackoffMs: 5000, logRetries: true, resetIntervalMs: 10000, maxTimeWithoutNewBlocksMs: 60000 }, config);
18
23
  this.logger = logger;
19
24
  const conns = config.urls.filter((url) => {
20
25
  if (!url) {
@@ -35,6 +40,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
35
40
  network: null,
36
41
  provider,
37
42
  index,
43
+ unreachable: false,
38
44
  };
39
45
  });
40
46
  this.activeFallbackProviderIndex = 0;
@@ -45,6 +51,25 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
45
51
  }
46
52
  return this._formatter;
47
53
  }
54
+ on(eventName, listener) {
55
+ let dieTimer = null;
56
+ const startDieTimer = (latestObservedBlockNumber) => {
57
+ if (dieTimer)
58
+ clearTimeout(dieTimer);
59
+ dieTimer = setTimeout(async () => {
60
+ const error = new noNewBlocksWhilePolling_error.NoNewBlocksWhilePollingError('No new blocks for a long time while polling', latestObservedBlockNumber);
61
+ this.emit('error', error);
62
+ }, this.config.maxTimeWithoutNewBlocksMs);
63
+ };
64
+ if (eventName === 'block') {
65
+ startDieTimer(-1);
66
+ super.on(eventName, function (...args) {
67
+ startDieTimer(args[0]);
68
+ return listener.apply(this, args);
69
+ });
70
+ }
71
+ return super.on(eventName, listener);
72
+ }
48
73
  get provider() {
49
74
  if (this.activeFallbackProviderIndex > this.fallbackProviders.length - 1) {
50
75
  this.activeFallbackProviderIndex = 0;
@@ -53,7 +78,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
53
78
  let attempt = 0;
54
79
  const isValid = (provider) => provider.network !== null &&
55
80
  provider.network.chainId === networks.getNetworkChain(this.config.network);
56
- while (!isValid(fallbackProvider) ||
81
+ while (!isValid(fallbackProvider) &&
57
82
  attempt < this.fallbackProviders.length) {
58
83
  fallbackProvider =
59
84
  this.fallbackProviders[this.activeFallbackProviderIndex];
@@ -74,12 +99,16 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
74
99
  this.activeFallbackProviderIndex++;
75
100
  this.logger.log(`Switched to next provider for execution layer`);
76
101
  }
102
+ errorShouldBeReThrown(error) {
103
+ return errors.isErrorHasCode(error) && errors.nonRetryableErrors.includes(error.code);
104
+ }
77
105
  async perform(method, params) {
78
- const retry = retrier.retrier(this.logger, this.config.maxRetries, this.config.minBackoffMs, this.config.maxBackoffMs, this.config.logRetries);
106
+ const retry = retrier.retrier(this.logger, this.config.maxRetries, this.config.minBackoffMs, this.config.maxBackoffMs, this.config.logRetries, (e) => this.errorShouldBeReThrown(e));
79
107
  let attempt = 0;
80
108
  // will perform maximum `this.config.maxRetries` retries for fetching data with single provider
81
109
  // after failure will switch to next provider
82
110
  // maximum number of switching is limited to total fallback provider count
111
+ let lastError;
83
112
  while (attempt < this.fallbackProviders.length) {
84
113
  try {
85
114
  attempt++;
@@ -88,21 +117,39 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
88
117
  return await retry(() => this.provider.provider.perform(method, params));
89
118
  }
90
119
  catch (e) {
120
+ if (this.errorShouldBeReThrown(e)) {
121
+ throw e;
122
+ }
91
123
  this.logger.error('Error while doing ETH1 RPC request. Will try to switch to another provider');
124
+ lastError = e;
92
125
  this.logger.error(e);
93
- this.switchToNextProvider();
126
+ // This check is needed to avoid multiple `switchToNextProvider` calls when doing one JSON-RPC batch.
127
+ // This can happen when multiple N calls to `perform` are batched in one JSON-RPC request and
128
+ // that request fails and throws `Error`. This `Error` is bubbled N times to corresponding `perform` calls.
129
+ // Without the following check, each `perform` call from batch catches `Error` and switches to the next provider,
130
+ // so during one batch multiple switching to next provider can occur, which is not needed.
131
+ if (this.lastPerformError != e) {
132
+ this.switchToNextProvider();
133
+ this.lastPerformError = e;
134
+ }
94
135
  }
95
136
  }
96
- throw new Error('All attempts to do ETH1 RPC request failed');
137
+ const allProvidersFailedError = new allProvidersFailed_error.AllProvidersFailedError('All attempts to do ETH1 RPC request failed');
138
+ allProvidersFailedError.originalError = lastError;
139
+ throw allProvidersFailedError;
97
140
  }
98
141
  async detectNetwork() {
99
- const results = await Promise.allSettled(this.fallbackProviders.map((c) => c.provider.getNetwork()));
100
- results.forEach((result, i) => {
142
+ const results = await Promise.allSettled(this.fallbackProviders
143
+ .filter((c) => !c.unreachable)
144
+ .map((c) => c.provider.getNetwork()));
145
+ results.forEach((result, index) => {
101
146
  if (result.status === 'fulfilled') {
102
- this.fallbackProviders[i].network = result.value;
147
+ this.fallbackProviders[index].network = result.value;
148
+ this.fallbackProviders[index].unreachable = false;
103
149
  }
104
150
  else {
105
- this.fallbackProviders[i].network = null;
151
+ this.fallbackProviders[index].network = null;
152
+ this.fallbackProviders[index].unreachable = true;
106
153
  }
107
154
  });
108
155
  let previousNetwork = null;
@@ -139,8 +186,26 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
139
186
  if (this.detectNetworkFirstRun) {
140
187
  this.detectNetworkFirstRun = false;
141
188
  }
189
+ if (this.resetTimer) {
190
+ clearTimeout(this.resetTimer);
191
+ }
192
+ this.resetTimer = setTimeout(() => {
193
+ this.resetFallbacks();
194
+ }, this.config.resetIntervalMs || 10000);
142
195
  return previousNetwork;
143
196
  }
197
+ resetFallbacks() {
198
+ if (this.resetTimer) {
199
+ clearTimeout(this.resetTimer);
200
+ }
201
+ this.fallbackProviders.forEach((fallbackProvider, index) => {
202
+ var _a;
203
+ if (!((_a = this.fallbackProviders[index].network) === null || _a === void 0 ? void 0 : _a.chainId)) {
204
+ this.fallbackProviders[index].unreachable = false;
205
+ }
206
+ });
207
+ this.activeFallbackProviderIndex = 0;
208
+ }
144
209
  networksEqual(networkA, networkB) {
145
210
  return networks.networksEqual(networkA, networkB);
146
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lido-nestjs/execution",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
+ "@ethersproject/logger": "^5.5.0",
37
38
  "@ethersproject/networks": "^5.5.2",
38
39
  "@ethersproject/properties": "^5.5.0",
39
40
  "@ethersproject/providers": "^5.5.3",