diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2b2ba..c359439 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 10318e7..0000000 --- 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 f3cc224..0000000 --- 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 f67b3d2..c74d982 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 4847bb3..bdd5cfc 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 };