From afcff8c81710bf67283060ef590bacf38fddf7fc Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 11 Mar 2025 08:41:59 -0600 Subject: [PATCH 1/2] Remove SubscribeBlockTracker We do not presently use this class in any repo under the MetaMask organization. In general we have found a polling-based approach to be more reliable than a subscription-based approach. --- CHANGELOG.md | 3 + src/SubscribeBlockTracker.test.ts | 3271 ----------------------------- src/SubscribeBlockTracker.ts | 332 --- src/index.ts | 1 - tests/withBlockTracker.ts | 77 +- 5 files changed, 9 insertions(+), 3675 deletions(-) delete mode 100644 src/SubscribeBlockTracker.test.ts delete mode 100644 src/SubscribeBlockTracker.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2b2ba6..c3594392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Removed +- **BREAKING:** Remove `SubscribeBlockTracker` + - Although we continue to maintain this, we have not used it internally for quite some time. In general we have found a polling-based approval to be reliable than a subscription-based approach. We recommend using `PollingBlockTracker` instead. ## [11.0.4] ### Changed diff --git a/src/SubscribeBlockTracker.test.ts b/src/SubscribeBlockTracker.test.ts deleted file mode 100644 index 10318e70..00000000 --- a/src/SubscribeBlockTracker.test.ts +++ /dev/null @@ -1,3271 +0,0 @@ -import { SubscribeBlockTracker } from '.'; -import buildDeferred from '../tests/buildDeferred'; -import EMPTY_FUNCTION from '../tests/emptyFunction'; -import recordCallsToSetTimeout from '../tests/recordCallsToSetTimeout'; -import { withSubscribeBlockTracker } from '../tests/withBlockTracker'; - -interface Sync { - oldBlock: string; - newBlock: string; -} - -const METHODS_TO_ADD_LISTENER = ['on', 'addListener'] as const; -const METHODS_TO_REMOVE_LISTENER = ['off', 'removeListener'] as const; -const METHODS_TO_GET_LATEST_BLOCK = [ - 'getLatestBlock', - 'checkForLatestBlock', -] as const; -const originalSetTimeout = setTimeout; - -describe('SubscribeBlockTracker', () => { - describe('constructor', () => { - it('should throw if given no options', () => { - expect(() => new SubscribeBlockTracker()).toThrow( - 'SubscribeBlockTracker - no provider specified.', - ); - }); - - it('should throw if given options but not given a provider', () => { - expect(() => new SubscribeBlockTracker({})).toThrow( - 'SubscribeBlockTracker - no provider specified.', - ); - }); - - it('should return a block tracker that is not running', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(({ blockTracker }) => { - expect(blockTracker.isRunning()).toBe(false); - }); - }); - }); - - describe('destroy', () => { - it('should stop the block tracker if any "latest" and "sync" events were added previously', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.isRunning()).toBe(true); - - await blockTracker.destroy(); - - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should not start a timer to clear the current block number if called after removing all listeners but before enough time passes that the cache would have been cleared', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockResetDuration = 500; - - await withSubscribeBlockTracker( - { - blockTracker: { - blockResetDuration, - }, - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - blockTracker.removeAllListeners(); - expect(setTimeoutRecorder.calls).not.toHaveLength(0); - - await blockTracker.destroy(); - - expect(setTimeoutRecorder.calls).toHaveLength(0); - await new Promise((resolve) => - originalSetTimeout(resolve, blockResetDuration), - ); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - }, - ); - }); - - it('should only clear the current block number if enough time passes after all "latest" and "sync" events are removed', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - blockTracker.removeAllListeners(); - await setTimeoutRecorder.next(); - - await blockTracker.destroy(); - - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - }); - - METHODS_TO_GET_LATEST_BLOCK.forEach((methodToGetLatestBlock) => { - describe(`${methodToGetLatestBlock}`, () => { - it('should start the block tracker immediately after being called', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const promiseToGetLatestBlock = - blockTracker[methodToGetLatestBlock](); - expect(blockTracker.isRunning()).toBe(true); - // We have to wait for the promise to resolve after the assertion - // because by the time this promise resolves, the block tracker isn't - // running anymore - await promiseToGetLatestBlock; - }); - }); - - it('should stop the block tracker automatically after its promise is fulfilled', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - await blockTracker[methodToGetLatestBlock](); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should resolve all returned promises when a new block is available', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promises = [ - blockTracker.getLatestBlock(), - blockTracker.getLatestBlock(), - ]; - - expect(await Promise.all(promises)).toStrictEqual(['0x1', '0x1']); - }, - ); - }); - - it('should reject the returned promise if the block tracker is destroyed in the meantime', async () => { - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const promiseToGetLatestBlock = - blockTracker[methodToGetLatestBlock](); - await blockTracker.destroy(); - - await expect(promiseToGetLatestBlock).rejects.toThrow( - 'Block tracker destroyed', - ); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should fetch the latest block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const latestBlockNumber = await blockTracker[ - methodToGetLatestBlock - ](); - expect(latestBlockNumber).toBe('0x0'); - }, - ); - }); - - it('should not ask for a new block number while the current block number is cached', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ provider, blockTracker }) => { - const requestSpy = jest.spyOn(provider, 'request'); - await blockTracker[methodToGetLatestBlock](); - await blockTracker[methodToGetLatestBlock](); - const requestsForLatestBlock = requestSpy.mock.calls.filter( - (args) => { - return args[0].method === 'eth_blockNumber'; - }, - ); - expect(requestsForLatestBlock).toHaveLength(1); - }); - }); - - it('should ask for a new block number after the cached one is cleared', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x0', - }, - { - methodName: 'eth_unsubscribe', - result: true, - }, - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - { - methodName: 'eth_subscribe', - result: '0x1', - }, - { - methodName: 'eth_unsubscribe', - result: true, - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const requestSpy = jest.spyOn(provider, 'request'); - await blockTracker[methodToGetLatestBlock](); - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - await blockTracker[methodToGetLatestBlock](); - const requestsForLatestBlock = requestSpy.mock.calls.filter( - (args) => { - return args[0].method === 'eth_blockNumber'; - }, - ); - expect(requestsForLatestBlock).toHaveLength(2); - }, - ); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toThrow(thrownError); - expect(listener).toHaveBeenCalledWith(thrownError); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toBe(thrownString); - expect(listener).toHaveBeenCalledWith(thrownString); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request for the latest block number, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toThrow('boom'); - expect(listener).toHaveBeenCalledWith(new Error('boom')); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request to subscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toThrow(thrownError); - expect(listener).toHaveBeenCalledWith(thrownError); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request to subscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toBe(thrownString); - expect(listener).toHaveBeenCalledWith(thrownString); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should reject if, while making the request to subscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker[methodToAddListener]('error', listener); - - const promiseForLatestBlock = - blockTracker[methodToGetLatestBlock](); - - await expect(promiseForLatestBlock).rejects.toThrow('boom'); - expect(listener).toHaveBeenCalledWith(new Error('boom')); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await blockTracker[methodToGetLatestBlock](); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await blockTracker[methodToGetLatestBlock](); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownString); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await blockTracker[methodToGetLatestBlock](); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - }); - - it('should update the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - await blockTracker[methodToGetLatestBlock](); - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - }, - ); - }); - - it('should clear the current block number some time after being called', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - await blockTracker[methodToGetLatestBlock](); - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - }); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - describe(`${methodToAddListener}`, () => { - describe('"latest"', () => { - it('should start the block tracker', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(({ blockTracker }) => { - blockTracker[methodToAddListener]('latest', EMPTY_FUNCTION); - - expect(blockTracker.isRunning()).toBe(true); - }); - }); - - it('should emit "latest" soon after being listened to', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const latestBlockNumber = await new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - expect(latestBlockNumber).toBe('0x0'); - }, - ); - }); - - it('should update the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - }, - ); - }); - - it('should not emit "latest" if the subscription id of an incoming message does not match the created subscription id', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x1', - result: { - number: '0x1', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0']); - }, - ); - }); - - it('should not emit "latest" if the incoming message has no params', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0']); - }, - ); - }); - - it('should re-throw any error out of band that occurs in the listener', async () => { - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const thrownError = new Error('boom'); - const promiseForCaughtError = new Promise((resolve) => { - recordCallsToSetTimeout({ - numAutomaticCalls: 1, - interceptCallback: (callback, stopPassingThroughCalls) => { - return async () => { - try { - await callback(); - } catch (error: unknown) { - resolve(error); - stopPassingThroughCalls(); - } - }; - }, - }); - }); - - blockTracker[methodToAddListener]('latest', () => { - throw thrownError; - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - }); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request for the latest block number, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - error: new Error('boom'), - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request to subscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request to subscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request to subscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - describe.each([ - ['not initialized with `usePastBlocks`', {}], - ['initialized with `usePastBlocks: false`', { usePastBlocks: false }], - ] as const)( - 'after a block number is cached if the block tracker was %s', - (_description, blockTrackerOptions) => { - it('should emit "latest" if the published block number is greater than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x1', - }, - }, - }); - }); - - await new Promise((resolve) => { - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - if (receivedBlockNumbers.length === 2) { - resolve(); - } - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0', '0x1']); - }, - ); - }); - - it('should not emit "latest" if the published block number is less than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x1']); - }, - ); - }); - - it('should not emit "latest" if the published block number is the same as the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0']); - }, - ); - }); - }, - ); - - describe('after a block number is cached if the block tracker was initialized with `usePastBlocks: true`', () => { - it('should emit "latest" if the published block number is greater than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x1', - }, - }, - }); - }); - - await new Promise((resolve) => { - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - if (receivedBlockNumbers.length === 2) { - resolve(); - } - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0', '0x1']); - }, - ); - }); - - it('should not emit "latest" if the published block number is less than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x1', '0x0']); - }, - ); - }); - - it('should not emit "latest" if the published block number is the same as the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const receivedBlockNumbers: string[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]( - 'latest', - (blockNumber: string) => { - receivedBlockNumbers.push(blockNumber); - }, - ); - }); - - expect(receivedBlockNumbers).toStrictEqual(['0x0']); - }, - ); - }); - }); - }); - - describe('"sync"', () => { - it('should start the block tracker', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(({ blockTracker }) => { - blockTracker[methodToAddListener]('sync', EMPTY_FUNCTION); - - expect(blockTracker.isRunning()).toBe(true); - }); - }); - - it('should emit "sync" soon after being listened to', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const sync = await new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); - }, - ); - }); - - it('should update the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - }, - ); - }); - - it('should not emit "sync" if the subscription id of an incoming message does not match the created subscription id', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x1', - result: { - number: '0x1', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - ]); - }, - ); - }); - - it('should not emit "sync" if the incoming message has no params', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - ]); - }, - ); - }); - - it('should re-throw any error out of band that occurs in the listener', async () => { - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const thrownError = new Error('boom'); - const promiseForCaughtError = new Promise((resolve) => { - recordCallsToSetTimeout({ - numAutomaticCalls: 1, - interceptCallback: (callback, stopPassingThroughCalls) => { - return async () => { - try { - await callback(); - } catch (error: unknown) { - resolve(error); - stopPassingThroughCalls(); - } - }; - }, - }); - }); - - blockTracker[methodToAddListener]('sync', () => { - throw thrownError; - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - }); - }); - - it(`should emit the "error" event and should not emit "sync" if, while making a request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "sync" if, while making a request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "sync" if, while making the request for the latest block number, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "sync" if, while making the request to subscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "latest" if, while making the request to subscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event and should not emit "sync" if, while making the request to subscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - describe.each([ - ['not initialized with `usePastBlocks`', {}], - ['initialized with `usePastBlocks: false`', { usePastBlocks: false }], - ] as const)( - 'after a block number is cached if the block tracker was %s', - (_description, blockTrackerOptions) => { - it('should emit "sync" if the published block number is greater than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x1', - }, - }, - }); - }); - - await new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - if (syncs.length === 2) { - resolve(); - } - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - { oldBlock: '0x0', newBlock: '0x1' }, - ]); - }, - ); - }); - - it('should not emit "sync" if the published block number is less than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x1' }, - ]); - }, - ); - }); - - it('should not emit "sync" if the published block number is the same as the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - ]); - }, - ); - }); - }, - ); - - describe('after a block number is cached if the block tracker was initialized with `usePastBlocks: true`', () => { - it('should emit "sync" if the published block number is greater than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x1', - }, - }, - }); - }); - - await new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - if (syncs.length === 2) { - resolve(); - } - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - { oldBlock: '0x0', newBlock: '0x1' }, - ]); - }, - ); - }); - - it('should emit "sync" if the published block number is less than the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x1', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x1' }, - { oldBlock: '0x1', newBlock: '0x0' }, - ]); - }, - ); - }); - - it('should not emit "sync" if the published block number is the same as the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - { - methodName: 'eth_subscribe', - result: '0x64', - }, - ], - }, - blockTracker: { usePastBlocks: true }, - }, - async ({ provider, blockTracker }) => { - const syncs: Sync[] = []; - - await new Promise((resolve) => { - blockTracker.on('_started', () => { - provider.emit('data', null, { - method: 'eth_subscription', - params: { - subscription: '0x64', - result: { - number: '0x0', - }, - }, - }); - resolve(); - }); - - blockTracker[methodToAddListener]('sync', (sync: Sync) => { - syncs.push(sync); - }); - }); - - expect(syncs).toStrictEqual([ - { oldBlock: null, newBlock: '0x0' }, - ]); - }, - ); - }); - }); - }); - - describe('some other event', () => { - it('should not start the block tracker', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(({ blockTracker }) => { - blockTracker[methodToAddListener]('somethingElse', EMPTY_FUNCTION); - - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should not update the current block number', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - blockTracker[methodToAddListener]( - 'somethingElse', - EMPTY_FUNCTION, - ); - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBeNull(); - }, - ); - }); - }); - }); - }); - - METHODS_TO_REMOVE_LISTENER.forEach((methodToRemoveListener) => { - describe(`${methodToRemoveListener}`, () => { - describe('"latest"', () => { - it('should stop the block tracker if the last instance of this event is removed', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const listener1 = EMPTY_FUNCTION; - const { promise: promiseForLatestBlock, resolve: listener2 } = - buildDeferred(); - - blockTracker.on('latest', listener1); - blockTracker.on('latest', listener2); - expect(blockTracker.isRunning()).toBe(true); - - await promiseForLatestBlock; - - blockTracker[methodToRemoveListener]('latest', listener1); - blockTracker[methodToRemoveListener]('latest', listener2); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should clear the current block number some time after the last instance of this event is removed', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - const listener1 = EMPTY_FUNCTION; - const { promise: promiseForLatestBlock, resolve: listener2 } = - buildDeferred(); - - blockTracker.on('latest', listener1); - blockTracker.on('latest', listener2); - await promiseForLatestBlock; - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - - blockTracker[methodToRemoveListener]('latest', listener1); - blockTracker[methodToRemoveListener]('latest', listener2); - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should not emit the "error" event (added via \`${methodToAddListener}\`) if the request to unsubscribe returns an error response`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - result: { - error: new Error('boom'), - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForLatestBlock, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('latest', listener); - await promiseForLatestBlock; - blockTracker[methodToRemoveListener]('latest', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - await expect(promiseForCaughtError).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForLatestBlock, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('latest', listener); - await promiseForLatestBlock; - blockTracker[methodToRemoveListener]('latest', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForLatestBlock, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('latest', listener); - await promiseForLatestBlock; - blockTracker[methodToRemoveListener]('latest', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForLatestBlock, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('latest', listener); - await promiseForLatestBlock; - blockTracker[methodToRemoveListener]('latest', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - }); - }); - - describe('"sync"', () => { - it('should stop the block tracker if the last instance of this event is removed', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const listener1 = EMPTY_FUNCTION; - const { promise: promiseForLatestBlock, resolve: listener2 } = - buildDeferred(); - - blockTracker.on('sync', listener1); - blockTracker.on('sync', listener2); - expect(blockTracker.isRunning()).toBe(true); - - await promiseForLatestBlock; - - blockTracker[methodToRemoveListener]('sync', listener1); - blockTracker[methodToRemoveListener]('sync', listener2); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should clear the current block number some time after the last instance of this event is removed', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - const listener1 = EMPTY_FUNCTION; - const { promise: promiseForLatestBlock, resolve: listener2 } = - buildDeferred(); - - blockTracker.on('sync', listener1); - blockTracker.on('sync', listener2); - await promiseForLatestBlock; - const currentBlockNumber = blockTracker.getCurrentBlock(); - expect(currentBlockNumber).toBe('0x0'); - - blockTracker[methodToRemoveListener]('sync', listener1); - blockTracker[methodToRemoveListener]('sync', listener2); - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should not emit the "error" event (added via \`${methodToAddListener}\`) if the request to unsubscribe returns an error response`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - result: { - error: new Error('boom'), - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForSync, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('sync', listener); - await promiseForSync; - blockTracker[methodToRemoveListener]('sync', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - await expect(promiseForCaughtError).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForSync, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('sync', listener); - await promiseForSync; - blockTracker[methodToRemoveListener]('sync', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForSync, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('sync', listener); - await promiseForSync; - blockTracker[methodToRemoveListener]('sync', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const { promise: promiseForSync, resolve: listener } = - buildDeferred(); - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - blockTracker.on('sync', listener); - await promiseForSync; - blockTracker[methodToRemoveListener]('sync', listener); - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - }); - }); - - describe('some other event', () => { - it('should not stop the block tracker', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const { promise: promiseForLatestBlock, resolve: listener1 } = - buildDeferred(); - const listener2 = EMPTY_FUNCTION; - - blockTracker.on('latest', listener1); - blockTracker.on('somethingElse', listener2); - expect(blockTracker.isRunning()).toBe(true); - - await promiseForLatestBlock; - - blockTracker[methodToRemoveListener]('somethingElse', listener2); - expect(blockTracker.isRunning()).toBe(true); - }); - }); - }); - }); - }); - - describe('once', () => { - describe('"latest"', () => { - it('should start and then stop the block tracker automatically', async () => { - // We stub 2 calls because SubscribeBlockTracker#_synchronize will make a - // call (to proceed to the next iteration) and BaseBlockTracker will - // make a call (to reset the current block number when the tracker is - // not running) - recordCallsToSetTimeout({ numAutomaticCalls: 2 }); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - blockTracker.once('latest', EMPTY_FUNCTION); - }); - - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should set the current block number and then clear it some time afterward', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toMatch(thrownString); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request for the latest block number, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request to subscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request to subscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownString); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "latest" if, while making the request to subscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForLatestBlock).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - }); - }); - - describe('"sync"', () => { - it('should start and then stop the block tracker automatically', async () => { - // We stub 2 calls because SubscribeBlockTracker#_synchronize will make a call - // (to proceed to the next iteration) and BaseBlockTracker will make a call - // (to reset the current block number when the tracker is not running) - recordCallsToSetTimeout({ numAutomaticCalls: 2 }); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker.on('_ended', resolve); - blockTracker.once('sync', EMPTY_FUNCTION); - }); - - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should set the current block number and then clear it some time afterward', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - - // For PollingBlockTracker, there are possibly multiple - // `setTimeout`s in play at this point. For SubscribeBlockTracker - // that is not the case, as it does not poll, but there is no harm - // in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - - METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "sync" if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "sync" if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownString); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "sync" if, while making the request for the latest block number, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "sync" if, while making the request to subscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownError = new Error('boom'); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownError; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownError); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not emit "sync" if, while making the request to subscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - const thrownString = 'boom'; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - implementation: () => { - throw thrownString; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe(thrownString); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should take a listener that is never called if, while making the request to subscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_subscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - await expect(promiseForSync).toNeverResolve(); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws an Error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider throws a string`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - implementation: () => { - throw 'boom'; - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError).toBe('boom'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) if, while making the request to unsubscribe, the provider rejects with an error`, async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_unsubscribe', - error: new Error('boom'), - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - await new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toBe('boom'); - }, - ); - }); - }); - }); - - describe('some other event', () => { - it('should never start the block tracker', async () => { - // We stub 2 calls because SubscribeBlockTracker#_synchronize will make a call - // (to proceed to the next iteration) and BaseBlockTracker will make a call - // (to reset the current block number when the tracker is not running) - recordCallsToSetTimeout({ numAutomaticCalls: 2 }); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - const listener = jest.fn(); - blockTracker.on('_ended', listener); - blockTracker.once('somethingElse', EMPTY_FUNCTION); - - expect(listener).not.toHaveBeenCalled(); - }); - }); - - it('should never set the current block number', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - blockTracker.once('somethingElse', EMPTY_FUNCTION); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - }); - }); - - describe('removeAllListeners', () => { - it('should stop the block tracker if any "latest" and "sync" events were added previously', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.isRunning()).toBe(true); - - blockTracker.removeAllListeners(); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should clear the current block number some time after all "latest" and "sync" events are removed', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - - blockTracker.removeAllListeners(); - // For PollingBlockTracker, there are possibly multiple `setTimeout`s - // in play at this point. For SubscribeBlockTracker that is not the - // case, as it does not poll, but there is no harm in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - - it('should stop the block tracker when all previously added "latest" and "sync" events are removed specifically', async () => { - recordCallsToSetTimeout(); - - await withSubscribeBlockTracker(async ({ blockTracker }) => { - await new Promise((resolve) => { - blockTracker.on('latest', EMPTY_FUNCTION); - blockTracker.on('sync', resolve); - }); - expect(blockTracker.isRunning()).toBe(true); - - blockTracker.removeAllListeners('latest'); - expect(blockTracker.isRunning()).toBe(true); - - blockTracker.removeAllListeners('sync'); - expect(blockTracker.isRunning()).toBe(false); - }); - }); - - it('should clear the current block number some time after all "latest" and "sync" events are removed specifically', async () => { - const setTimeoutRecorder = recordCallsToSetTimeout(); - const blockTrackerOptions = { - pollingInterval: 100, - blockResetDuration: 200, - }; - - await withSubscribeBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - blockTracker: blockTrackerOptions, - }, - async ({ blockTracker }) => { - blockTracker.on('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('sync', resolve); - }); - expect(blockTracker.getCurrentBlock()).toBe('0x0'); - - blockTracker.removeAllListeners('latest'); - blockTracker.removeAllListeners('sync'); - // For PollingBlockTracker, there are possibly multiple `setTimeout`s - // in play at this point. For SubscribeBlockTracker that is not the - // case, as it does not poll, but there is no harm in doing this. - await setTimeoutRecorder.nextMatchingDuration( - blockTrackerOptions.blockResetDuration, - ); - expect(blockTracker.getCurrentBlock()).toBeNull(); - }, - ); - }); - }); -}); diff --git a/src/SubscribeBlockTracker.ts b/src/SubscribeBlockTracker.ts deleted file mode 100644 index f3cc2241..00000000 --- a/src/SubscribeBlockTracker.ts +++ /dev/null @@ -1,332 +0,0 @@ -import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; -import SafeEventEmitter from '@metamask/safe-event-emitter'; -import { - createDeferredPromise, - type DeferredPromise, - type Json, - type JsonRpcNotification, -} from '@metamask/utils'; -import getCreateRandomId from 'json-rpc-random-id'; - -import type { BlockTracker } from './BlockTracker'; - -const createRandomId = getCreateRandomId(); - -const sec = 1000; - -const blockTrackerEvents: (string | symbol)[] = ['sync', 'latest']; - -export interface SubscribeBlockTrackerOptions { - provider?: SafeEventEmitterProvider; - blockResetDuration?: number; - usePastBlocks?: boolean; -} - -interface SubscriptionNotificationParams { - [key: string]: Json; - subscription: string; - result: { number: string }; -} - -type InternalListener = (value: string) => void; - -export class SubscribeBlockTracker - extends SafeEventEmitter - implements BlockTracker -{ - private _isRunning: boolean; - - private readonly _blockResetDuration: number; - - private readonly _usePastBlocks: boolean; - - private _currentBlock: string | null; - - private _blockResetTimeout?: ReturnType; - - private readonly _provider: SafeEventEmitterProvider; - - private _subscriptionId: string | null; - - readonly #internalEventListeners: InternalListener[] = []; - - #pendingLatestBlock?: Omit, 'resolve'>; - - constructor(opts: SubscribeBlockTrackerOptions = {}) { - // parse + validate args - if (!opts.provider) { - throw new Error('SubscribeBlockTracker - no provider specified.'); - } - - super(); - - // config - this._blockResetDuration = opts.blockResetDuration || 20 * sec; - this._usePastBlocks = opts.usePastBlocks || false; - // state - this._currentBlock = null; - this._isRunning = false; - - // bind functions for internal use - this._onNewListener = this._onNewListener.bind(this); - this._onRemoveListener = this._onRemoveListener.bind(this); - this._resetCurrentBlock = this._resetCurrentBlock.bind(this); - - // listen for handler changes - this._setupInternalEvents(); - - // config - this._provider = opts.provider; - this._subscriptionId = null; - } - - async destroy() { - this._cancelBlockResetTimeout(); - await this._maybeEnd(); - super.removeAllListeners(); - this.#rejectPendingLatestBlock(new Error('Block tracker destroyed')); - } - - isRunning(): boolean { - return this._isRunning; - } - - getCurrentBlock(): string | null { - return this._currentBlock; - } - - async getLatestBlock(): Promise { - // return if available - if (this._currentBlock) { - return this._currentBlock; - } else if (this.#pendingLatestBlock) { - return await this.#pendingLatestBlock.promise; - } - - const { resolve, reject, promise } = createDeferredPromise({ - suppressUnhandledRejection: true, - }); - this.#pendingLatestBlock = { reject, promise }; - - // wait for a new latest block - const onLatestBlock = (value: string) => { - this.#removeInternalListener(onLatestBlock); - resolve(value); - this.#pendingLatestBlock = undefined; - }; - this.#addInternalListener(onLatestBlock); - this.once('latest', onLatestBlock); - return await promise; - } - - // dont allow module consumer to remove our internal event listeners - removeAllListeners(eventName?: string | symbol) { - // perform default behavior, preserve fn arity - if (eventName) { - super.removeAllListeners(eventName); - } else { - super.removeAllListeners(); - } - - // re-add internal events - this._setupInternalEvents(); - // trigger stop check just in case - this._onRemoveListener(); - - return this; - } - - private _setupInternalEvents(): void { - // first remove listeners for idempotence - this.removeListener('newListener', this._onNewListener); - this.removeListener('removeListener', this._onRemoveListener); - // then add them - this.on('newListener', this._onNewListener); - this.on('removeListener', this._onRemoveListener); - } - - private _onNewListener(eventName: string | symbol): void { - // `newListener` is called *before* the listener is added - if (blockTrackerEvents.includes(eventName)) { - // TODO: Handle dangling promise - this._maybeStart(); - } - } - - private _onRemoveListener(): void { - // `removeListener` is called *after* the listener is removed - if (this._getBlockTrackerEventCount() > 0) { - return; - } - this._maybeEnd(); - } - - private async _maybeStart(): Promise { - if (this._isRunning) { - return; - } - this._isRunning = true; - // cancel setting latest block to stale - this._cancelBlockResetTimeout(); - await this._start(); - this.emit('_started'); - } - - private async _maybeEnd(): Promise { - if (!this._isRunning) { - return; - } - this._isRunning = false; - this._setupBlockResetTimeout(); - await this._end(); - this.emit('_ended'); - } - - private _getBlockTrackerEventCount(): number { - return ( - blockTrackerEvents - .map((eventName) => this.listeners(eventName)) - .flat() - // internal listeners are not included in the count - .filter((listener) => - this.#internalEventListeners.every( - (internalListener) => !Object.is(internalListener, listener), - ), - ).length - ); - } - - private _shouldUseNewBlock(newBlock: string) { - const currentBlock = this._currentBlock; - if (!currentBlock) { - return true; - } - const newBlockInt = hexToInt(newBlock); - const currentBlockInt = hexToInt(currentBlock); - - return ( - (this._usePastBlocks && newBlockInt < currentBlockInt) || - newBlockInt > currentBlockInt - ); - } - - private _newPotentialLatest(newBlock: string): void { - if (!this._shouldUseNewBlock(newBlock)) { - return; - } - this._setCurrentBlock(newBlock); - } - - private _setCurrentBlock(newBlock: string): void { - const oldBlock = this._currentBlock; - this._currentBlock = newBlock; - this.emit('latest', newBlock); - this.emit('sync', { oldBlock, newBlock }); - } - - private _setupBlockResetTimeout(): void { - // clear any existing timeout - this._cancelBlockResetTimeout(); - // clear latest block when stale - this._blockResetTimeout = setTimeout( - this._resetCurrentBlock, - this._blockResetDuration, - ); - - // nodejs - dont hold process open - if (this._blockResetTimeout.unref) { - this._blockResetTimeout.unref(); - } - } - - private _cancelBlockResetTimeout(): void { - if (this._blockResetTimeout) { - clearTimeout(this._blockResetTimeout); - } - } - - private _resetCurrentBlock(): void { - this._currentBlock = null; - } - - async checkForLatestBlock(): Promise { - return await this.getLatestBlock(); - } - - private async _start(): Promise { - if (this._subscriptionId === undefined || this._subscriptionId === null) { - try { - const blockNumber = (await this._call('eth_blockNumber')) as string; - this._subscriptionId = (await this._call( - 'eth_subscribe', - 'newHeads', - )) as string; - this._provider.on('data', this._handleSubData.bind(this)); - this._newPotentialLatest(blockNumber); - } catch (e) { - this.emit('error', e); - this.#rejectPendingLatestBlock(e); - } - } - } - - private async _end() { - if (this._subscriptionId !== null && this._subscriptionId !== undefined) { - try { - await this._call('eth_unsubscribe', this._subscriptionId); - this._subscriptionId = null; - } catch (e) { - this.emit('error', e); - this.#rejectPendingLatestBlock(e); - } - } - } - - private async _call(method: string, ...params: Json[]): Promise { - return this._provider.request({ - id: createRandomId(), - method, - params, - jsonrpc: '2.0', - }); - } - - private _handleSubData( - _: unknown, - response: JsonRpcNotification, - ): void { - if ( - response.method === 'eth_subscription' && - response.params?.subscription === this._subscriptionId - ) { - this._newPotentialLatest(response.params.result.number); - } - } - - #addInternalListener(listener: InternalListener) { - this.#internalEventListeners.push(listener); - } - - #removeInternalListener(listener: InternalListener) { - this.#internalEventListeners.splice( - this.#internalEventListeners.indexOf(listener), - 1, - ); - } - - #rejectPendingLatestBlock(error: unknown) { - this.#pendingLatestBlock?.reject(error); - this.#pendingLatestBlock = undefined; - } -} - -/** - * Converts a number represented as a string in hexadecimal format into a native - * number. - * - * @param hexInt - The hex string. - * @returns The number. - */ -function hexToInt(hexInt: string): number { - return Number.parseInt(hexInt, 16); -} diff --git a/src/index.ts b/src/index.ts index f67b3d2c..c74d9828 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ export * from './PollingBlockTracker'; -export * from './SubscribeBlockTracker'; export * from './BlockTracker'; diff --git a/tests/withBlockTracker.ts b/tests/withBlockTracker.ts index 4847bb3a..bdd5cfca 100644 --- a/tests/withBlockTracker.ts +++ b/tests/withBlockTracker.ts @@ -7,11 +7,8 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import type { Json } from '@metamask/utils'; import util from 'util'; -import type { - PollingBlockTrackerOptions, - SubscribeBlockTrackerOptions, -} from '../src'; -import { PollingBlockTracker, SubscribeBlockTracker } from '../src'; +import type { PollingBlockTrackerOptions } from '../src'; +import { PollingBlockTracker } from '../src'; interface WithPollingBlockTrackerOptions { provider?: FakeProviderOptions; @@ -23,16 +20,6 @@ type WithPollingBlockTrackerCallback = (args: { blockTracker: PollingBlockTracker; }) => void | Promise; -interface WithSubscribeBlockTrackerOptions { - provider?: FakeProviderOptions; - blockTracker?: SubscribeBlockTrackerOptions; -} - -type WithSubscribeBlockTrackerCallback = (args: { - provider: SafeEventEmitterProvider; - blockTracker: SubscribeBlockTracker; -}) => void | Promise; - /** * An object that allows specifying the behavior of a specific invocation of * `request`. The `methodName` always identifies the stub, but the behavior @@ -151,7 +138,7 @@ function getFakeProvider({ * tracker. * @returns The provider and block tracker. */ -async function withPollingBlockTracker( +export async function withPollingBlockTracker( options: WithPollingBlockTrackerOptions, callback: WithPollingBlockTrackerCallback, ): Promise; @@ -164,11 +151,12 @@ async function withPollingBlockTracker( * tracker. * @returns The provider and block tracker. */ -async function withPollingBlockTracker( +export async function withPollingBlockTracker( callback: WithPollingBlockTrackerCallback, ): Promise; + /* eslint-disable-next-line jsdoc/require-jsdoc */ -async function withPollingBlockTracker( +export async function withPollingBlockTracker( ...args: | [WithPollingBlockTrackerOptions, WithPollingBlockTrackerCallback] | [WithPollingBlockTrackerCallback] @@ -189,56 +177,3 @@ async function withPollingBlockTracker( const callbackArgs = { provider, blockTracker }; await callback(callbackArgs); } - -/** - * Calls the given function with a built-in SubscribeBlockTracker, ensuring that - * all listeners that are on the block tracker are removed and any timers or - * loops that are running within the block tracker are properly stopped. - * - * @param options - Options that allow configuring the block tracker or - * provider. - * @param callback - A callback which will be called with the built block - * tracker. - * @returns The provider and block tracker. - */ -async function withSubscribeBlockTracker( - options: WithSubscribeBlockTrackerOptions, - callback: WithSubscribeBlockTrackerCallback, -): Promise; -/** - * Calls the given function with a built-in SubscribeBlockTracker, ensuring that - * all listeners that are on the block tracker are removed and any timers or - * loops that are running within the block tracker are properly stopped. - * - * @param callback - A callback which will be called with the built block - * tracker. - * @returns The provider and block tracker. - */ -async function withSubscribeBlockTracker( - callback: WithSubscribeBlockTrackerCallback, -): Promise; -/* eslint-disable-next-line jsdoc/require-jsdoc */ -async function withSubscribeBlockTracker( - ...args: - | [WithSubscribeBlockTrackerOptions, WithSubscribeBlockTrackerCallback] - | [WithSubscribeBlockTrackerCallback] -) { - const [options, callback] = args.length === 2 ? args : [{}, args[0]]; - const provider = - options.provider === undefined - ? getFakeProvider() - : getFakeProvider(options.provider); - - const blockTrackerOptions = - options.blockTracker === undefined - ? { provider } - : { - provider, - ...options.blockTracker, - }; - const blockTracker = new SubscribeBlockTracker(blockTrackerOptions); - const callbackArgs = { provider, blockTracker }; - await callback(callbackArgs); -} - -export { withPollingBlockTracker, withSubscribeBlockTracker }; From 437de6011ab68ff03bc5ec0549b0acbb86f4afd7 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 11 Mar 2025 12:19:48 -0600 Subject: [PATCH 2/2] PollingBlockTracker: Stop wrapping errors If PollingBlockTracker fails to make a request, the error is wrapped with a marker a more descriptive message (including the stack trace) before being emitted under the "error" event or logged. This wrapping may have been added in the past to assist with debugging, but it is not necessary now, and it complicates the tests. --- src/PollingBlockTracker.test.ts | 620 +++++--------------------------- src/PollingBlockTracker.ts | 14 +- 2 files changed, 94 insertions(+), 540 deletions(-) diff --git a/src/PollingBlockTracker.test.ts b/src/PollingBlockTracker.test.ts index e3c36161..e367efae 100644 --- a/src/PollingBlockTracker.test.ts +++ b/src/PollingBlockTracker.test.ts @@ -371,7 +371,8 @@ describe('PollingBlockTracker', () => { }); METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws an Error`, async () => { + it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -381,7 +382,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw new Error('boom'); + throw thrownError; }, }, { @@ -392,60 +393,20 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = blockTracker.getLatestBlock(); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom\n/u, - ); - const latestBlock = await promiseForLatestBlock; - expect(latestBlock).toBe('0x0'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw 'boom'; - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = blockTracker.getLatestBlock(); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider rejects with an error`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -454,7 +415,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -464,59 +425,21 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = blockTracker.getLatestBlock(); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); }); - it('should log an error if, while making a request for the latest block number, the provider throws an Error and there is nothing listening to "error"', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - jest.spyOn(console, 'error').mockImplementation(EMPTY_FUNCTION); - - blockTracker.getLatestBlock(); - await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); - }); - - expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), - ); - }, - ); - }); - - it('should log an error the request for the latest block number throws a string and there is nothing listening to "error"', async () => { + it('should log an error if, while making a request for the latest block number, the provider throws and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -526,7 +449,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, ], @@ -541,17 +464,14 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); }); - it('should log an error if, while requesting the latest block number, the provider rejects with an error and there is nothing listening to "error"', async () => { + it('should log an error if, while requesting the latest block number, the provider rejects and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -560,7 +480,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, ], }, @@ -574,11 +494,7 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); @@ -1217,7 +1133,8 @@ describe('PollingBlockTracker', () => { ); }); - it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws an Error`, async () => { + it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1227,7 +1144,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw new Error('boom'); + throw thrownError; }, }, { @@ -1238,64 +1155,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = new Promise((resolve) => { blockTracker[methodToAddListener]('latest', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom\n/u, - ); - const latestBlock = await promiseForLatestBlock; - expect(latestBlock).toBe('0x0'); - }, - ); - }); - - it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw 'boom'; - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker[methodToAddListener]('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider rejects with an error`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1304,7 +1179,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -1314,64 +1189,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = new Promise((resolve) => { blockTracker[methodToAddListener]('latest', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); - it('should log an error if, while making a request for the latest block number, the provider throws an Error and there is nothing listening to "error"', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - jest.spyOn(console, 'error').mockImplementation(EMPTY_FUNCTION); - - blockTracker[methodToAddListener]('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); - }); - - expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), - ); - }, - ); - }); - - it('should log an error if, while making a request for the latest block number, the provider throws a string and there is nothing listening to "error"', async () => { + it('should log an error if, while making a request for the latest block number, the provider throws and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1381,7 +1214,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, { @@ -1400,17 +1233,14 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); }); it('should log an error if, while making the request for the latest block number, the provider rejects with an error and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1419,7 +1249,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -1437,11 +1267,7 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); @@ -1849,7 +1675,8 @@ describe('PollingBlockTracker', () => { ); }); - it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws an Error`, async () => { + it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1859,7 +1686,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw new Error('boom'); + throw thrownError; }, }, { @@ -1870,64 +1697,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForSync = new Promise((resolve) => { blockTracker[methodToAddListener]('sync', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom\n/u, - ); - const sync = await promiseForSync; - expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); - }, - ); - }); - - it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider throws a string`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw 'boom'; - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker[methodToAddListener]('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ); const sync = await promiseForSync; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); }, ); }); it(`should emit the "error" event and should not kill the block tracker if, while making the request for the latest block number, the provider rejects with an error`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -1946,60 +1731,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForSync = new Promise((resolve) => { blockTracker[methodToAddListener]('sync', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ); const sync = await promiseForSync; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); }, ); }); - it('should log an error if, while making a request for the latest block number, the provider throws an Error and there is nothing listening to "error"', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - ], - }, - }, - async ({ blockTracker }) => { - jest.spyOn(console, 'error').mockImplementation(EMPTY_FUNCTION); - - blockTracker[methodToAddListener]('sync', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); - }); - - expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), - ); - }, - ); - }); - - it('should log an error if, while making a request for the latest block number, the provider throws a string and there is nothing listening to "error"', async () => { + it('should log an error if, while making a request for the latest block number, the provider throws and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2009,7 +1756,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, ], @@ -2024,17 +1771,14 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); }); it('should log an error if, while making the request for the latest block number, the provider rejects with an error and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2043,7 +1787,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, ], }, @@ -2057,11 +1801,7 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); @@ -2666,46 +2406,8 @@ describe('PollingBlockTracker', () => { }); METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForLatestBlock = new Promise((resolve) => { - blockTracker.once('latest', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom\n/u, - ); - const latestBlock = await promiseForLatestBlock; - expect(latestBlock).toBe('0x0'); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws a string`, async () => { + it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2715,7 +2417,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, { @@ -2726,25 +2428,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = new Promise((resolve) => { blockTracker.once('latest', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider rejects with an error`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2753,7 +2452,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -2763,65 +2462,23 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForLatestBlock = new Promise((resolve) => { blockTracker.once('latest', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ); const latestBlock = await promiseForLatestBlock; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(latestBlock).toBe('0x0'); }, ); }); }); - it('should log an error if, while making a request for the latest block number, the provider throws an Error and there is nothing listening to "error"', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - jest.spyOn(console, 'error').mockImplementation(EMPTY_FUNCTION); - - blockTracker.once('latest', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); - }); - - expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), - ); - }, - ); - }); - - it('should log an error if, while making a request for the latest block number, the provider throws a string and there is nothing listening to "error"', async () => { + it('should log an error if, while making a request for the latest block number, the provider throws and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2831,7 +2488,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, { @@ -2850,17 +2507,14 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); }); it('should log an error if, while making the request for the latest block number, the provider rejects with an error and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -2869,7 +2523,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -2887,11 +2541,7 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); @@ -2953,46 +2603,8 @@ describe('PollingBlockTracker', () => { }); METHODS_TO_ADD_LISTENER.forEach((methodToAddListener) => { - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws an Error`, async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); - - const promiseForSync = new Promise((resolve) => { - blockTracker.once('sync', resolve); - }); - - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom\n/u, - ); - const sync = await promiseForSync; - expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); - }, - ); - }); - - it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws a string`, async () => { + it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider throws`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -3002,7 +2614,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, { @@ -3013,25 +2625,22 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForSync = new Promise((resolve) => { blockTracker.once('sync', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ); const sync = await promiseForSync; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); }, ); }); it(`should emit the "error" event (added via \`${methodToAddListener}\`) and should not throw if, while making the request for the latest block number, the provider rejects with an error`, async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -3040,7 +2649,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -3050,65 +2659,23 @@ describe('PollingBlockTracker', () => { }, }, async ({ blockTracker }) => { - const promiseForCaughtError = new Promise((resolve) => { - blockTracker[methodToAddListener]('error', resolve); - }); + const errorListener = jest.fn(); + blockTracker[methodToAddListener]('error', errorListener); const promiseForSync = new Promise((resolve) => { blockTracker.once('sync', resolve); }); - const caughtError = await promiseForCaughtError; - expect(caughtError.message).toMatch( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ); const sync = await promiseForSync; + expect(errorListener).toHaveBeenCalledWith(thrownError); expect(sync).toStrictEqual({ oldBlock: null, newBlock: '0x0' }); }, ); }); }); - it('should log an error if, while making a request for the latest block number, the provider throws an Error and there is nothing listening to "error"', async () => { - recordCallsToSetTimeout({ numAutomaticCalls: 1 }); - - await withPollingBlockTracker( - { - provider: { - stubs: [ - { - methodName: 'eth_blockNumber', - implementation: () => { - throw new Error('boom'); - }, - }, - { - methodName: 'eth_blockNumber', - result: '0x0', - }, - ], - }, - }, - async ({ blockTracker }) => { - jest.spyOn(console, 'error').mockImplementation(EMPTY_FUNCTION); - - blockTracker.once('sync', EMPTY_FUNCTION); - await new Promise((resolve) => { - blockTracker.on('_waitingForNextIteration', resolve); - }); - - expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), - ); - }, - ); - }); - - it('should log an error if, while making a request for the latest block number, the provider throws a string and there is nothing listening to "error"', async () => { + it('should log an error if, while making a request for the latest block number, the provider throws and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -3118,7 +2685,7 @@ describe('PollingBlockTracker', () => { { methodName: 'eth_blockNumber', implementation: () => { - throw 'boom'; + throw thrownError; }, }, { @@ -3137,17 +2704,14 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nboom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); }); it('should log an error if, while making the request for the latest block number, the provider rejects with an error and there is nothing listening to "error"', async () => { + const thrownError = new Error('boom'); recordCallsToSetTimeout({ numAutomaticCalls: 1 }); await withPollingBlockTracker( @@ -3156,7 +2720,7 @@ describe('PollingBlockTracker', () => { stubs: [ { methodName: 'eth_blockNumber', - error: new Error('boom'), + error: thrownError, }, { methodName: 'eth_blockNumber', @@ -3174,11 +2738,7 @@ describe('PollingBlockTracker', () => { }); expect(console.error).toHaveBeenCalledWith( - expect.objectContaining({ - message: expect.stringMatching( - /^PollingBlockTracker - encountered an error while attempting to update latest block:\nError: boom/u, - ), - }), + 'Error updating latest block: boom', ); }, ); diff --git a/src/PollingBlockTracker.ts b/src/PollingBlockTracker.ts index b0ad70d2..9b60d336 100644 --- a/src/PollingBlockTracker.ts +++ b/src/PollingBlockTracker.ts @@ -319,17 +319,11 @@ export class PollingBlockTracker try { await this._updateLatestBlock(); - } catch (err: any) { - const newErr = new Error( - `PollingBlockTracker - encountered an error while attempting to update latest block:\n${ - err.stack ?? err - }`, - ); - + } catch (error: unknown) { try { - this.emit('error', newErr); - } catch (emitErr) { - console.error(newErr); + this.emit('error', error); + } catch { + console.error(`Error updating latest block: ${getErrorMessage(error)}`); } interval = this._retryTimeout;