@lido-nestjs/execution 1.15.0 → 1.17.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.
@@ -2,3 +2,4 @@ export * from './all-providers-failed.error';
2
2
  export * from './fetch.error';
3
3
  export * from './no-new-blocks-while-polling.error';
4
4
  export * from './request-timeout.error';
5
+ export * from './transaction-wait-timeout.error';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Thrown when waitForTransactionWithFallback times out.
3
+ * lastError distinguishes network issues (present) from slow tx (null).
4
+ */
5
+ export declare class TransactionWaitTimeoutError extends Error {
6
+ name: string;
7
+ txHash: string;
8
+ timeoutMs: number;
9
+ lastError: Error | null;
10
+ constructor(txHash: string, timeoutMs: number, lastError: Error | null);
11
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * Thrown when waitForTransactionWithFallback times out.
7
+ * lastError distinguishes network issues (present) from slow tx (null).
8
+ */
9
+ class TransactionWaitTimeoutError extends Error {
10
+ constructor(txHash, timeoutMs, lastError) {
11
+ const baseMessage = `Transaction ${txHash} not confirmed within ${timeoutMs}ms`;
12
+ const errorContext = lastError
13
+ ? `. Last provider error: ${lastError.message}`
14
+ : ' (no provider errors, transaction may still be pending)';
15
+ super(baseMessage + errorContext);
16
+ this.name = 'TransactionWaitTimeoutError';
17
+ this.txHash = txHash;
18
+ this.timeoutMs = timeoutMs;
19
+ this.lastError = lastError;
20
+ Object.setPrototypeOf(this, new.target.prototype);
21
+ }
22
+ }
23
+
24
+ exports.TransactionWaitTimeoutError = TransactionWaitTimeoutError;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './interfaces/fallback-provider';
7
7
  export * from './interfaces/simple-fallback-provider-config';
8
8
  export * from './interfaces/module.options';
9
9
  export * from './interfaces/non-empty-array';
10
+ export * from './interfaces/wait-for-transaction';
10
11
  export * from './ethers/fee-history';
11
12
  export * from './ethers/block-tag';
12
13
  export * from './error';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var allProvidersFailed_error = require('./error/all-providers-failed.error.js');
12
12
  var fetch_error = require('./error/fetch.error.js');
13
13
  var noNewBlocksWhilePolling_error = require('./error/no-new-blocks-while-polling.error.js');
14
14
  var requestTimeout_error = require('./error/request-timeout.error.js');
15
+ var transactionWaitTimeout_error = require('./error/transaction-wait-timeout.error.js');
15
16
 
16
17
 
17
18
 
@@ -39,3 +40,4 @@ exports.AllProvidersFailedError = allProvidersFailed_error.AllProvidersFailedErr
39
40
  exports.FetchError = fetch_error.FetchError;
40
41
  exports.NoNewBlocksWhilePollingError = noNewBlocksWhilePolling_error.NoNewBlocksWhilePollingError;
41
42
  exports.RequestTimeoutError = requestTimeout_error.RequestTimeoutError;
43
+ exports.TransactionWaitTimeoutError = transactionWaitTimeout_error.TransactionWaitTimeoutError;
@@ -15,4 +15,5 @@ export interface SimpleFallbackProviderConfig {
15
15
  fetchMiddlewares?: MiddlewareCallback<Promise<any>>[];
16
16
  maxTimeWithoutNewBlocksMs?: number;
17
17
  requestTimeoutMs?: number;
18
+ instanceLabel?: string;
18
19
  }
@@ -0,0 +1,14 @@
1
+ import { TransactionReceipt } from '@ethersproject/abstract-provider';
2
+ export interface WaitForTransactionOptions {
3
+ /** Max wait time in ms. @default 60000 */
4
+ timeout?: number;
5
+ /** Polling interval in ms. @default 3000 */
6
+ pollInterval?: number;
7
+ /** Required confirmations. @default 1 */
8
+ confirmations?: number;
9
+ }
10
+ export interface WaitForTransactionResult {
11
+ receipt: TransactionReceipt;
12
+ pollCount: number;
13
+ elapsedMs: number;
14
+ }
@@ -10,6 +10,7 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber';
10
10
  import { Deferrable } from '@ethersproject/properties';
11
11
  import { EventType, Listener } from '@ethersproject/abstract-provider';
12
12
  import { FeeHistory } from '../ethers/fee-history';
13
+ import { WaitForTransactionOptions, WaitForTransactionResult } from '../interfaces/wait-for-transaction';
13
14
  import { TraceConfig, TraceResult } from '../interfaces/debug-traces';
14
15
  import { FallbackProviderEvents } from '../events';
15
16
  /**
@@ -46,6 +47,7 @@ export declare class SimpleFallbackJsonRpcBatchProvider extends BaseProvider {
46
47
  constructor(config: SimpleFallbackProviderConfig, logger: LoggerService);
47
48
  static _formatter: Formatter | null;
48
49
  static getFormatter(): Formatter;
50
+ protected formatLog(message: string, providerIndex?: number): string;
49
51
  on(eventName: EventType, listener: Listener): this;
50
52
  getFeeHistory(blockCount: number, newestBlock?: string | null | number, rewardPercentiles?: number[]): Promise<FeeHistory>;
51
53
  getDebugTraceBlockByHash(blockHash: string, traceConfig: Partial<TraceConfig>): Promise<TraceResult[]>;
@@ -61,4 +63,16 @@ export declare class SimpleFallbackJsonRpcBatchProvider extends BaseProvider {
61
63
  protected networksEqual(networkA: Network, networkB: Network): boolean;
62
64
  get activeProviderIndex(): number;
63
65
  get eventEmitter(): SimpleFallbackJsonRpcBatchProviderEventEmitter;
66
+ /**
67
+ * Waits for transaction confirmation with fallback provider support.
68
+ *
69
+ * Use instead of tx.wait() which hangs forever when providers fail:
70
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1055 - polling calls getTransactionReceipt
71
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1060 - .catch(e => emit("error", e)) swallows error
72
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1313 - waits for event that never comes
73
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1412 - timeout=0 by default, no reject
74
+ *
75
+ * This method polls via perform() with fallback and always returns/throws.
76
+ */
77
+ waitForTransactionWithFallback(txHash: string, options?: WaitForTransactionOptions): Promise<WaitForTransactionResult>;
64
78
  }
@@ -7,18 +7,21 @@ var providers = require('@ethersproject/providers');
7
7
  var extendedJsonRpcBatchProvider = require('./extended-json-rpc-batch-provider.js');
8
8
  var common = require('@nestjs/common');
9
9
  var retrier = require('../common/retrier.js');
10
+ var sleep = require('../common/sleep.js');
10
11
  var formatterWithEip1898 = require('../ethers/formatter-with-eip1898.js');
11
12
  var networks = require('../common/networks.js');
12
13
  var noNewBlocksWhilePolling_error = require('../error/no-new-blocks-while-polling.error.js');
13
14
  var errors = require('../common/errors.js');
14
15
  var allProvidersFailed_error = require('../error/all-providers-failed.error.js');
15
16
  var requestTimeout_error = require('../error/request-timeout.error.js');
17
+ var transactionWaitTimeout_error = require('../error/transaction-wait-timeout.error.js');
16
18
  var feeHistory = require('../ethers/fee-history.js');
17
19
  var debugTraceBlockByHash = require('../ethers/debug-trace-block-by-hash.js');
18
20
  var events = require('events');
19
21
 
20
22
  exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchProvider extends providers.BaseProvider {
21
23
  constructor(config, logger) {
24
+ var _a;
22
25
  super(config.network);
23
26
  this.detectNetworkFirstRun = true;
24
27
  this.resetTimer = null;
@@ -55,6 +58,22 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
55
58
  };
56
59
  });
57
60
  this.activeFallbackProviderIndex = 0;
61
+ // Log initialization info
62
+ const configInfo = {
63
+ providers: conns.length,
64
+ network: this.config.network,
65
+ requestPolicy: this.config.requestPolicy,
66
+ maxRetries: this.config.maxRetries,
67
+ minBackoffMs: this.config.minBackoffMs,
68
+ maxBackoffMs: this.config.maxBackoffMs,
69
+ logRetries: this.config.logRetries,
70
+ resetIntervalMs: this.config.resetIntervalMs,
71
+ fetchMiddlewares: ((_a = this.config.fetchMiddlewares) === null || _a === void 0 ? void 0 : _a.length) || 0,
72
+ maxTimeWithoutNewBlocksMs: this.config.maxTimeWithoutNewBlocksMs,
73
+ requestTimeoutMs: this.config.requestTimeoutMs || 'disabled',
74
+ instanceLabel: this.config.instanceLabel || 'none',
75
+ };
76
+ this.logger.log(this.formatLog(`Initialized SimpleFallbackJsonRpcBatchProvider: ${JSON.stringify(configInfo)}`));
58
77
  }
59
78
  static getFormatter() {
60
79
  if (this._formatter == null) {
@@ -62,6 +81,19 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
62
81
  }
63
82
  return this._formatter;
64
83
  }
84
+ formatLog(message, providerIndex) {
85
+ const parts = [];
86
+ if (this.config.instanceLabel) {
87
+ parts.push(`[${this.config.instanceLabel}]`);
88
+ }
89
+ if (providerIndex !== undefined) {
90
+ parts.push(`[provider:${providerIndex}]`);
91
+ }
92
+ if (parts.length > 0) {
93
+ return `${parts.join('')} ${message}`;
94
+ }
95
+ return message;
96
+ }
65
97
  on(eventName, listener) {
66
98
  let dieTimer = null;
67
99
  const startDieTimer = (latestObservedBlockNumber) => {
@@ -110,11 +142,13 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
110
142
  }
111
143
  switchToNextProvider() {
112
144
  if (this.fallbackProviders.length === 1) {
113
- this.logger.warn('Will not switch to next provider. No valid backup provider provided.');
145
+ this.logger.warn(this.formatLog('Will not switch to next provider. No valid backup provider provided.'));
114
146
  return;
115
147
  }
116
- this.activeFallbackProviderIndex++;
117
- this.logger.log(`Switched to next provider for execution layer`);
148
+ const oldIndex = this.activeFallbackProviderIndex;
149
+ this.activeFallbackProviderIndex =
150
+ (this.activeFallbackProviderIndex + 1) % this.fallbackProviders.length;
151
+ this.logger.log(this.formatLog(`Switched provider: [${oldIndex}] -> [${this.activeFallbackProviderIndex}] (total: ${this.fallbackProviders.length})`));
118
152
  }
119
153
  isNonRetryableError(error) {
120
154
  return (!errors.isEthersServerError(error) &&
@@ -141,9 +175,11 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
141
175
  try {
142
176
  let performRetryAttempt = 0;
143
177
  attempt++;
178
+ // Log which provider we're attempting to use
179
+ this.logger.log(this.formatLog(`Attempting request (attempt ${attempt}/${this.fallbackProviders.length})`, this.activeFallbackProviderIndex));
144
180
  // awaiting is extremely important here
145
181
  // without it, the error will not be caught in current try-catch scope
146
- return await retry(() => {
182
+ const result = await retry(() => {
147
183
  const provider = this.provider;
148
184
  const event = {
149
185
  action: 'fallback-provider:request',
@@ -162,6 +198,9 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
162
198
  }
163
199
  return performPromise;
164
200
  });
201
+ // Log successful request
202
+ this.logger.log(this.formatLog(`Request successful after ${performRetryAttempt} retry attempt(s)`, this.activeFallbackProviderIndex));
203
+ return result;
165
204
  }
166
205
  catch (e) {
167
206
  this.lastError = e;
@@ -173,10 +212,22 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
173
212
  error: e,
174
213
  };
175
214
  this._eventEmitter.emit('rpc', event);
215
+ // Log context (label + provider index) synchronously before error object
216
+ // to ensure proper ordering in async logging systems
217
+ this.logger.error(this.formatLog(`Non-retryable error occurred`, this.activeFallbackProviderIndex));
218
+ this.logger.error(e);
176
219
  throw e;
177
220
  }
178
- this.logger.error('Error while doing ETH1 RPC request. Will try to switch to another provider');
179
- this.logger.error(e);
221
+ // Log context (label + provider index) synchronously before error object
222
+ // to ensure proper ordering in async logging systems
223
+ if (e instanceof requestTimeout_error.RequestTimeoutError) {
224
+ this.logger.error(this.formatLog(`Request timeout after ${e.timeoutMs}ms. Will switch to next provider.`, this.activeFallbackProviderIndex));
225
+ this.logger.error(e);
226
+ }
227
+ else {
228
+ this.logger.error(this.formatLog(`Error occurred. Will switch to next provider.`, this.activeFallbackProviderIndex));
229
+ this.logger.error(e);
230
+ }
180
231
  // This check is needed to avoid multiple `switchToNextProvider` calls when doing one JSON-RPC batch.
181
232
  // This can happen when multiple N calls to `perform` are batched in one JSON-RPC request and
182
233
  // that request fails and throws `Error`. This `Error` is bubbled N times to corresponding `perform` calls.
@@ -234,7 +285,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
234
285
  if (this.detectNetworkFirstRun) {
235
286
  throw new Error(`Fallback provider [${index}] network is different to other provider's networks`);
236
287
  }
237
- this.logger.warn(`Fallback provider [${index}] network is different to other provider's networks`);
288
+ this.logger.warn(this.formatLog(`Fallback provider [${index}] network is different to other provider's networks`));
238
289
  }
239
290
  }
240
291
  else {
@@ -261,7 +312,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
261
312
  if (this.resetTimer) {
262
313
  clearTimeout(this.resetTimer);
263
314
  }
264
- this.fallbackProviders.forEach((fallbackProvider, index) => {
315
+ this.fallbackProviders.forEach((_, index) => {
265
316
  var _a;
266
317
  if (!((_a = this.fallbackProviders[index].network) === null || _a === void 0 ? void 0 : _a.chainId)) {
267
318
  this.fallbackProviders[index].unreachable = false;
@@ -278,6 +329,49 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
278
329
  get eventEmitter() {
279
330
  return this._eventEmitter;
280
331
  }
332
+ /**
333
+ * Waits for transaction confirmation with fallback provider support.
334
+ *
335
+ * Use instead of tx.wait() which hangs forever when providers fail:
336
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1055 - polling calls getTransactionReceipt
337
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1060 - .catch(e => emit("error", e)) swallows error
338
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1313 - waits for event that never comes
339
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1412 - timeout=0 by default, no reject
340
+ *
341
+ * This method polls via perform() with fallback and always returns/throws.
342
+ */
343
+ async waitForTransactionWithFallback(txHash, options = {}) {
344
+ const { timeout = 60000, pollInterval = 3000, confirmations = 1, } = options;
345
+ const startTime = Date.now();
346
+ let lastError = null;
347
+ let pollCount = 0;
348
+ this.logger.log(this.formatLog(`Starting waitForTransactionWithFallback for ${txHash} (timeout: ${timeout}ms, pollInterval: ${pollInterval}ms, confirmations: ${confirmations})`));
349
+ while (Date.now() - startTime < timeout) {
350
+ pollCount++;
351
+ try {
352
+ // Uses perform() which handles fallback switching automatically
353
+ const receipt = await this.getTransactionReceipt(txHash);
354
+ if (receipt && receipt.confirmations >= confirmations) {
355
+ const elapsedMs = Date.now() - startTime;
356
+ this.logger.log(this.formatLog(`Transaction ${txHash} confirmed after ${pollCount} polls (${elapsedMs}ms, ${receipt.confirmations} confirmations)`));
357
+ return { receipt, pollCount, elapsedMs };
358
+ }
359
+ lastError = null;
360
+ }
361
+ catch (error) {
362
+ // All providers failed - log and retry until timeout
363
+ lastError = error;
364
+ this.logger.warn(this.formatLog(`waitForTransactionWithFallback poll #${pollCount} failed for ${txHash}: ${error}`));
365
+ }
366
+ await sleep.sleep(pollInterval);
367
+ }
368
+ const elapsedMs = Date.now() - startTime;
369
+ const errorContext = lastError
370
+ ? `All providers failed on last poll: ${lastError.message}`
371
+ : 'Transaction not found in any block';
372
+ this.logger.error(this.formatLog(`waitForTransactionWithFallback timeout for ${txHash} after ${pollCount} polls (${elapsedMs}ms). ${errorContext}`));
373
+ throw new transactionWaitTimeout_error.TransactionWaitTimeoutError(txHash, timeout, lastError);
374
+ }
281
375
  };
282
376
  exports.SimpleFallbackJsonRpcBatchProvider._formatter = null;
283
377
  exports.SimpleFallbackJsonRpcBatchProvider = tslib.__decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lido-nestjs/execution",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",