@lido-nestjs/execution 1.16.0 → 1.18.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;
@@ -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
  /**
@@ -62,4 +63,16 @@ export declare class SimpleFallbackJsonRpcBatchProvider extends BaseProvider {
62
63
  protected networksEqual(networkA: Network, networkB: Network): boolean;
63
64
  get activeProviderIndex(): number;
64
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>;
65
78
  }
@@ -7,12 +7,14 @@ 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');
@@ -164,8 +166,10 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
164
166
  ]);
165
167
  }
166
168
  async perform(method, params) {
169
+ var _a, _b;
167
170
  const retry = retrier.retrier(this.logger, this.config.maxRetries, this.config.minBackoffMs, this.config.maxBackoffMs, this.config.logRetries, (e) => this.isNonRetryableError(e));
168
171
  let attempt = 0;
172
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, this.formatLog(`RPC call: ${method}`));
169
173
  // will perform maximum `this.config.maxRetries` retries for fetching data with single provider
170
174
  // after failure will switch to next provider
171
175
  // maximum number of switching is limited to total fallback provider count
@@ -174,7 +178,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
174
178
  let performRetryAttempt = 0;
175
179
  attempt++;
176
180
  // Log which provider we're attempting to use
177
- this.logger.log(this.formatLog(`Attempting request (attempt ${attempt}/${this.fallbackProviders.length})`, this.activeFallbackProviderIndex));
181
+ this.logger.log(this.formatLog(`Attempting ${method} (attempt ${attempt}/${this.fallbackProviders.length})`, this.activeFallbackProviderIndex));
178
182
  // awaiting is extremely important here
179
183
  // without it, the error will not be caught in current try-catch scope
180
184
  const result = await retry(() => {
@@ -197,7 +201,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
197
201
  return performPromise;
198
202
  });
199
203
  // Log successful request
200
- this.logger.log(this.formatLog(`Request successful after ${performRetryAttempt} retry attempt(s)`, this.activeFallbackProviderIndex));
204
+ this.logger.log(this.formatLog(`${method} successful after ${performRetryAttempt} retry attempt(s)`, this.activeFallbackProviderIndex));
201
205
  return result;
202
206
  }
203
207
  catch (e) {
@@ -212,18 +216,18 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
212
216
  this._eventEmitter.emit('rpc', event);
213
217
  // Log context (label + provider index) synchronously before error object
214
218
  // to ensure proper ordering in async logging systems
215
- this.logger.error(this.formatLog(`Non-retryable error occurred`, this.activeFallbackProviderIndex));
219
+ this.logger.error(this.formatLog(`${method} non-retryable error occurred`, this.activeFallbackProviderIndex));
216
220
  this.logger.error(e);
217
221
  throw e;
218
222
  }
219
223
  // Log context (label + provider index) synchronously before error object
220
224
  // to ensure proper ordering in async logging systems
221
225
  if (e instanceof requestTimeout_error.RequestTimeoutError) {
222
- this.logger.error(this.formatLog(`Request timeout after ${e.timeoutMs}ms. Will switch to next provider.`, this.activeFallbackProviderIndex));
226
+ this.logger.error(this.formatLog(`${method} timeout after ${e.timeoutMs}ms. Will switch to next provider.`, this.activeFallbackProviderIndex));
223
227
  this.logger.error(e);
224
228
  }
225
229
  else {
226
- this.logger.error(this.formatLog(`Error occurred. Will switch to next provider.`, this.activeFallbackProviderIndex));
230
+ this.logger.error(this.formatLog(`${method} error occurred. Will switch to next provider.`, this.activeFallbackProviderIndex));
227
231
  this.logger.error(e);
228
232
  }
229
233
  // This check is needed to avoid multiple `switchToNextProvider` calls when doing one JSON-RPC batch.
@@ -237,7 +241,7 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
237
241
  }
238
242
  }
239
243
  }
240
- const allProvidersFailedError = new allProvidersFailed_error.AllProvidersFailedError('All attempts to do ETH1 RPC request failed');
244
+ const allProvidersFailedError = new allProvidersFailed_error.AllProvidersFailedError(`All attempts to do ETH1 RPC request failed for ${method}`);
241
245
  allProvidersFailedError.cause = this.lastError;
242
246
  const event = {
243
247
  action: 'fallback-provider:request:failed:all',
@@ -327,6 +331,59 @@ exports.SimpleFallbackJsonRpcBatchProvider = class SimpleFallbackJsonRpcBatchPro
327
331
  get eventEmitter() {
328
332
  return this._eventEmitter;
329
333
  }
334
+ /**
335
+ * Waits for transaction confirmation with fallback provider support.
336
+ *
337
+ * Use instead of tx.wait() which hangs forever when providers fail:
338
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1055 - polling calls getTransactionReceipt
339
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1060 - .catch(e => emit("error", e)) swallows error
340
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1313 - waits for event that never comes
341
+ * - node_modules/@ethersproject/providers/src.ts/base-provider.ts:1412 - timeout=0 by default, no reject
342
+ *
343
+ * This method polls via perform() with fallback and always returns/throws.
344
+ */
345
+ async waitForTransactionWithFallback(txHash, options = {}) {
346
+ const { timeout = 60000, pollInterval = 3000, confirmations = 1, } = options;
347
+ const startTime = Date.now();
348
+ let lastError = null;
349
+ let pollCount = 0;
350
+ this.logger.log(this.formatLog(`Starting waitForTransactionWithFallback for ${txHash} (timeout: ${timeout}ms, pollInterval: ${pollInterval}ms, confirmations: ${confirmations})`));
351
+ while (Date.now() - startTime < timeout) {
352
+ pollCount++;
353
+ try {
354
+ // Uses perform() which handles fallback switching automatically
355
+ const receipt = await this.getTransactionReceipt(txHash);
356
+ if (!receipt) {
357
+ // Transaction pending in mempool, not yet included in a block
358
+ lastError = null;
359
+ await sleep.sleep(pollInterval);
360
+ continue;
361
+ }
362
+ if (receipt.confirmations < confirmations) {
363
+ // Transaction mined but waiting for more confirmations
364
+ lastError = null;
365
+ await sleep.sleep(pollInterval);
366
+ continue;
367
+ }
368
+ // Transaction confirmed with enough confirmations
369
+ const elapsedMs = Date.now() - startTime;
370
+ this.logger.log(this.formatLog(`Transaction ${txHash} confirmed after ${pollCount} polls (${elapsedMs}ms, ${receipt.confirmations} confirmations)`));
371
+ return { receipt, pollCount, elapsedMs };
372
+ }
373
+ catch (error) {
374
+ // All providers failed - log and retry until timeout
375
+ lastError = error;
376
+ this.logger.warn(this.formatLog(`waitForTransactionWithFallback poll #${pollCount} failed for ${txHash}: ${error}`));
377
+ await sleep.sleep(pollInterval);
378
+ }
379
+ }
380
+ const elapsedMs = Date.now() - startTime;
381
+ const errorContext = lastError
382
+ ? `All providers failed on last poll: ${lastError.message}`
383
+ : 'Transaction not found in any block';
384
+ this.logger.error(this.formatLog(`waitForTransactionWithFallback timeout for ${txHash} after ${pollCount} polls (${elapsedMs}ms). ${errorContext}`));
385
+ throw new transactionWaitTimeout_error.TransactionWaitTimeoutError(txHash, timeout, lastError);
386
+ }
330
387
  };
331
388
  exports.SimpleFallbackJsonRpcBatchProvider._formatter = null;
332
389
  exports.SimpleFallbackJsonRpcBatchProvider = tslib.__decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lido-nestjs/execution",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",