diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index 2e8a05309b..1a7a1fc208 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -49,7 +49,6 @@ "@metamask/utils": "^8.3.0" }, "devDependencies": { - "@metamask/approval-controller": "^5.1.3", "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^17.2.1", "@metamask/selected-network-controller": "^9.0.0", @@ -66,7 +65,6 @@ "typescript": "~4.8.4" }, "peerDependencies": { - "@metamask/approval-controller": "^5.1.2", "@metamask/network-controller": "^17.2.0", "@metamask/selected-network-controller": "^9.0.0" }, diff --git a/packages/queued-request-controller/src/QueuedRequestController.test.ts b/packages/queued-request-controller/src/QueuedRequestController.test.ts index 4839d82654..cffd9ac564 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.test.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.test.ts @@ -1,11 +1,10 @@ -import type { AddApprovalRequest } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { defaultState as defaultNetworkState, - type NetworkControllerGetNetworkConfigurationByNetworkClientId, type NetworkControllerGetStateAction, type NetworkControllerSetActiveNetworkAction, } from '@metamask/network-controller'; +import type { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller'; import { createDeferredPromise } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -33,60 +32,40 @@ describe('QueuedRequestController', () => { }); describe('enqueueRequest', () => { - it('counts a request as queued during processing', async () => { + it('skips the queue if the queue is empty and no request is being processed', async () => { const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), }; const controller = new QueuedRequestController(options); await controller.enqueueRequest(buildRequest(), async () => { - expect(controller.state.queuedRequestCount).toBe(1); + expect(controller.state.queuedRequestCount).toBe(0); }); expect(controller.state.queuedRequestCount).toBe(0); }); - it('counts a request as queued while waiting on another request to finish processing', async () => { + it('skips the queue if the queue is empty and the request being processed has the same origin', async () => { const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), }; const controller = new QueuedRequestController(options); - const { promise: firstRequestProcessing, resolve: resolveFirstRequest } = - createDeferredPromise(); + // Trigger first request const firstRequest = controller.enqueueRequest( buildRequest(), - () => firstRequestProcessing, - ); - const secondRequest = controller.enqueueRequest( - buildRequest(), - async () => { - expect(controller.state.queuedRequestCount).toBe(1); - }, + () => new Promise((resolve) => setTimeout(resolve, 10)), ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); - expect(controller.state.queuedRequestCount).toBe(2); + await controller.enqueueRequest(buildRequest(), async () => { + expect(controller.state.queuedRequestCount).toBe(0); + }); + expect(controller.state.queuedRequestCount).toBe(0); - resolveFirstRequest(); await firstRequest; - await secondRequest; }); - it('runs the next request immediately when the queue is empty', async () => { - const options: QueuedRequestControllerOptions = { - messenger: buildQueuedRequestControllerMessenger(), - }; - - const controller = new QueuedRequestController(options); - - // Mock requestNext function - const requestNext = jest.fn(() => Promise.resolve()); - - await controller.enqueueRequest(buildRequest(), requestNext); - - // Expect that the request was called - expect(requestNext).toHaveBeenCalledTimes(1); - }); - - it('switches network if a request comes in for a different selected chain', async () => { + it('switches network if a request comes in for a different network client', async () => { const mockSetActiveNetwork = jest.fn(); const { messenger } = buildControllerMessenger({ networkControllerGetState: jest.fn().mockReturnValue({ @@ -94,26 +73,34 @@ describe('QueuedRequestController', () => { selectedNetworkClientId: 'selectedNetworkClientId', }), networkControllerSetActiveNetwork: mockSetActiveNetwork, + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((_origin) => 'differentNetworkClientId'), }); + const onNetworkSwitched = jest.fn(); + messenger.subscribe( + 'QueuedRequestController:networkSwitched', + onNetworkSwitched, + ); const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(messenger), }; const controller = new QueuedRequestController(options); await controller.enqueueRequest( - { - ...buildRequest(), - networkClientId: 'differentNetworkClientId', - }, + buildRequest(), () => new Promise((resolve) => setTimeout(resolve, 10)), ); expect(mockSetActiveNetwork).toHaveBeenCalledWith( 'differentNetworkClientId', ); + expect(onNetworkSwitched).toHaveBeenCalledWith( + 'differentNetworkClientId', + ); }); - it('does not switch networks if a request comes in for the same chain', async () => { + it('does not switch networks if a request comes in for the same network client', async () => { const mockSetActiveNetwork = jest.fn(); const { messenger } = buildControllerMessenger({ networkControllerGetState: jest.fn().mockReturnValue({ @@ -121,120 +108,424 @@ describe('QueuedRequestController', () => { selectedNetworkClientId: 'selectedNetworkClientId', }), networkControllerSetActiveNetwork: mockSetActiveNetwork, + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((_origin) => 'selectedNetworkClientId'), }); + const onNetworkSwitched = jest.fn(); + messenger.subscribe( + 'QueuedRequestController:networkSwitched', + onNetworkSwitched, + ); const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(messenger), }; const controller = new QueuedRequestController(options); await controller.enqueueRequest( - { - ...buildRequest(), - networkClientId: 'selectedNetworkClientId', - }, + buildRequest(), () => new Promise((resolve) => setTimeout(resolve, 10)), ); expect(mockSetActiveNetwork).not.toHaveBeenCalled(); + expect(onNetworkSwitched).not.toHaveBeenCalled(); }); - it('does not switch networks if the switch chain confirmation is rejected', async () => { - const mockSetActiveNetwork = jest.fn(); - const { messenger } = buildControllerMessenger({ - approvalControllerAddRequest: jest - .fn() - .mockRejectedValue(new Error('Rejected')), - networkControllerGetState: jest.fn().mockReturnValue({ - ...cloneDeep(defaultNetworkState), - selectedNetworkClientId: 'selectedNetworkClientId', - }), - networkControllerSetActiveNetwork: mockSetActiveNetwork, - }); + it('queues request if a request from another origin is being processed', async () => { const options: QueuedRequestControllerOptions = { - messenger: buildQueuedRequestControllerMessenger(messenger), + messenger: buildQueuedRequestControllerMessenger(), }; const controller = new QueuedRequestController(options); + // Trigger first request + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://exampleorigin1.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); - await expect(() => - controller.enqueueRequest( - { - ...buildRequest(), - networkClientId: 'differentNetworkClientId', - }, - () => new Promise((resolve) => setTimeout(resolve, 10)), - ), - ).rejects.toThrow('Rejected'); + const secondRequestNext = jest.fn(); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://exampleorigin2.metamask.io' }, + secondRequestNext, + ); - expect(mockSetActiveNetwork).not.toHaveBeenCalled(); + expect(controller.state.queuedRequestCount).toBe(1); + expect(secondRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + await secondRequest; + }); + + it('drains batch from queue when current batch finishes', async () => { + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(), + }; + const controller = new QueuedRequestController(options); + // Trigger first batch + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstbatch.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstbatch.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 20)), + ); + // ensure first batch requests skip queue + expect(controller.state.queuedRequestCount).toBe(0); + const thirdRequestNext = jest.fn(); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondbatch.metamask.io' }, + thirdRequestNext, + ); + const fourthRequestNext = jest.fn(); + const fourthRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondbatch.metamask.io' }, + fourthRequestNext, + ); + // ensure test starts with a two-request batch queued up + expect(controller.state.queuedRequestCount).toBe(2); + expect(thirdRequestNext).not.toHaveBeenCalled(); + expect(fourthRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + + // ensure second batch is still queued when first batch hasn't finished yet + expect(controller.state.queuedRequestCount).toBe(2); + expect(thirdRequestNext).not.toHaveBeenCalled(); + expect(fourthRequestNext).not.toHaveBeenCalled(); + + await secondRequest; + await thirdRequest; + await fourthRequest; + + expect(controller.state.queuedRequestCount).toBe(0); + expect(thirdRequestNext).toHaveBeenCalled(); + expect(fourthRequestNext).toHaveBeenCalled(); }); - it('runs each request sequentially in the correct order', async () => { + it('drains batch from queue when current batch finishes with requests out-of-order', async () => { const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), }; + const controller = new QueuedRequestController(options); + // Trigger first batch + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstbatch.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 20)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstbatch.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first batch requests skip queue + expect(controller.state.queuedRequestCount).toBe(0); + const thirdRequestNext = jest.fn(); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondbatch.metamask.io' }, + thirdRequestNext, + ); + const fourthRequestNext = jest.fn(); + const fourthRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondbatch.metamask.io' }, + fourthRequestNext, + ); + // ensure test starts with a two-request batch queued up + expect(controller.state.queuedRequestCount).toBe(2); + expect(thirdRequestNext).not.toHaveBeenCalled(); + expect(fourthRequestNext).not.toHaveBeenCalled(); + + await secondRequest; + // ensure second batch is still queued when first batch hasn't finished yet + expect(controller.state.queuedRequestCount).toBe(2); + expect(thirdRequestNext).not.toHaveBeenCalled(); + expect(fourthRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + await thirdRequest; + await fourthRequest; + + expect(controller.state.queuedRequestCount).toBe(0); + expect(thirdRequestNext).toHaveBeenCalled(); + expect(fourthRequestNext).toHaveBeenCalled(); + }); + + it('processes requests from each batch in parallel', async () => { + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(), + }; const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + }, + ); + // ensure first batch requests skip queue + expect(controller.state.queuedRequestCount).toBe(0); + const { + promise: secondRequestProcessing, + resolve: resolveSecondRequest, + } = createDeferredPromise(); + const secondRequestNext = jest + .fn() + .mockImplementation(async () => secondRequestProcessing); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + const { promise: thirdRequestProcessing, resolve: resolveThirdRequest } = + createDeferredPromise(); + const thirdRequestNext = jest + .fn() + .mockImplementation(async () => thirdRequestProcessing); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + thirdRequestNext, + ); + const { + promise: fourthRequestProcessing, + resolve: resolveFourthRequest, + } = createDeferredPromise(); + const fourthRequestNext = jest + .fn() + .mockImplementation(async () => fourthRequestProcessing); + const fourthRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + fourthRequestNext, + ); + expect(controller.state.queuedRequestCount).toBe(3); + await firstRequest; - // Mock requestNext function with resolved promises - // Use an array to track the order of execution - const executionOrder: string[] = []; + // resolve and await requests in the wrong order + // If requests were executed one-at-a-time, this would deadlock + resolveFourthRequest(); + await fourthRequest; + resolveThirdRequest(); + await thirdRequest; + resolveSecondRequest(); + await secondRequest; + + expect(controller.state.queuedRequestCount).toBe(0); + }); - // Enqueue requests - controller.enqueueRequest(buildRequest(), async () => { - executionOrder.push('Request 1 Start'); + it('preserves request order within each batch', async () => { + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(), + }; + const controller = new QueuedRequestController(options); + const executionOrder: string[] = []; + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + async () => { + executionOrder.push('Request 1 Start'); + await new Promise((resolve) => setTimeout(resolve, 10)); + }, + ); + // ensure first batch requests skip queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest.fn().mockImplementation(async () => { + executionOrder.push('Request 2 Start'); + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + const thirdRequestNext = jest.fn().mockImplementation(async () => { + executionOrder.push('Request 3 Start'); + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + thirdRequestNext, + ); + const fourthRequestNext = jest.fn().mockImplementation(async () => { + executionOrder.push('Request 4 Start'); await new Promise((resolve) => setTimeout(resolve, 10)); - executionOrder.push('Request 1 End'); }); + const fourthRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + fourthRequestNext, + ); + expect(controller.state.queuedRequestCount).toBe(3); - await controller.enqueueRequest(buildRequest(), async () => { + await Promise.all([ + firstRequest, + secondRequest, + thirdRequest, + fourthRequest, + ]); + + expect(executionOrder).toStrictEqual([ + 'Request 1 Start', + 'Request 2 Start', + 'Request 3 Start', + 'Request 4 Start', + ]); + }); + + it('preserves request order even when interlaced with requests from other origins', async () => { + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(), + }; + const controller = new QueuedRequestController(options); + const executionOrder: string[] = []; + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + async () => { + executionOrder.push('Request 1 Start'); + await new Promise((resolve) => setTimeout(resolve, 10)); + }, + ); + // ensure first batch requests skip queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest.fn().mockImplementation(async () => { executionOrder.push('Request 2 Start'); await new Promise((resolve) => setTimeout(resolve, 10)); - executionOrder.push('Request 2 End'); }); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + const thirdRequestNext = jest.fn().mockImplementation(async () => { + executionOrder.push('Request 3 Start'); + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + thirdRequestNext, + ); + // ensure test starts with two batches queued up + expect(controller.state.queuedRequestCount).toBe(2); + + await Promise.all([firstRequest, secondRequest, thirdRequest]); - // Assert that the execution order is correct expect(executionOrder).toStrictEqual([ 'Request 1 Start', - 'Request 1 End', 'Request 2 Start', - 'Request 2 End', + 'Request 3 Start', ]); }); - describe('error handling', () => { - it('handles errors when a request fails', async () => { - const options: QueuedRequestControllerOptions = { - messenger: buildQueuedRequestControllerMessenger(), - }; + it('switches network if a new batch has a different network client', async () => { + const mockSetActiveNetwork = jest.fn(); + const { messenger } = buildControllerMessenger({ + networkControllerGetState: jest.fn().mockReturnValue({ + ...cloneDeep(defaultNetworkState), + selectedNetworkClientId: 'selectedNetworkClientId', + }), + networkControllerSetActiveNetwork: mockSetActiveNetwork, + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((origin) => + origin === 'https://secondorigin.metamask.io' + ? 'differentNetworkClientId' + : 'selectedNetworkClientId', + ), + }); + const onNetworkSwitched = jest.fn(); + messenger.subscribe( + 'QueuedRequestController:networkSwitched', + onNetworkSwitched, + ); + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(messenger), + }; + const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + // ensure test starts with one request queued up + expect(controller.state.queuedRequestCount).toBe(1); + expect(secondRequestNext).not.toHaveBeenCalled(); + expect(mockSetActiveNetwork).not.toHaveBeenCalled(); - const controller = new QueuedRequestController(options); + await firstRequest; + await secondRequest; - // Mock a request that throws an error - const requestWithError = jest.fn(() => - Promise.reject(new Error('Request failed')), - ); + expect(mockSetActiveNetwork).toHaveBeenCalledWith( + 'differentNetworkClientId', + ); + expect(onNetworkSwitched).toHaveBeenCalledWith( + 'differentNetworkClientId', + ); + }); - // Enqueue the request - await expect(() => - controller.enqueueRequest(buildRequest(), requestWithError), - ).rejects.toThrow(new Error('Request failed')); - expect(controller.state.queuedRequestCount).toBe(0); + it('does not switch networks if a new batch has the same network client', async () => { + const mockSetActiveNetwork = jest.fn(); + const { messenger } = buildControllerMessenger({ + networkControllerGetState: jest.fn().mockReturnValue({ + ...cloneDeep(defaultNetworkState), + selectedNetworkClientId: 'selectedNetworkClientId', + }), + networkControllerSetActiveNetwork: mockSetActiveNetwork, + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation(() => 'selectedNetworkClientId'), }); + const onNetworkSwitched = jest.fn(); + messenger.subscribe( + 'QueuedRequestController:networkSwitched', + onNetworkSwitched, + ); + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(messenger), + }; + const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'firstorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + // ensure test starts with one request queued up + expect(controller.state.queuedRequestCount).toBe(1); + expect(secondRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + await secondRequest; - it('rejects requests that require a switch if they are missing network configuration', async () => { - const mockSetActiveNetwork = jest.fn(); + expect(mockSetActiveNetwork).not.toHaveBeenCalled(); + expect(onNetworkSwitched).not.toHaveBeenCalled(); + }); + + describe('when the network switch for a single request fails', () => { + it('throws error', async () => { + const switchError = new Error('switch error'); const { messenger } = buildControllerMessenger({ networkControllerGetState: jest.fn().mockReturnValue({ ...cloneDeep(defaultNetworkState), selectedNetworkClientId: 'selectedNetworkClientId', }), - networkControllerGetNetworkConfigurationByNetworkClientId: ( - networkClientId, - ) => - networkClientId === 'selectedNetworkClientId' - ? { chainId: '0x999', rpcUrl: 'metamask.io', ticker: 'TEST' } - : undefined, - networkControllerSetActiveNetwork: mockSetActiveNetwork, + networkControllerSetActiveNetwork: jest + .fn() + .mockRejectedValue(switchError), + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((_origin) => 'differentNetworkClientId'), }); const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(messenger), @@ -243,48 +534,182 @@ describe('QueuedRequestController', () => { await expect(() => controller.enqueueRequest( - { - ...buildRequest(), - networkClientId: 'differentNetworkClientId', - }, - () => new Promise((resolve) => setTimeout(resolve, 10)), + { ...buildRequest(), origin: 'https://example.metamask.io' }, + jest.fn(), ), - ).rejects.toThrow( - 'Missing network configuration for differentNetworkClientId', + ).rejects.toThrow(switchError); + }); + + it('correctly processes the next item in the queue', async () => { + const switchError = new Error('switch error'); + const { messenger } = buildControllerMessenger({ + networkControllerGetState: jest.fn().mockReturnValue({ + ...cloneDeep(defaultNetworkState), + selectedNetworkClientId: 'selectedNetworkClientId', + }), + networkControllerSetActiveNetwork: jest + .fn() + .mockRejectedValue(switchError), + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((origin) => + origin === 'https://firstorigin.metamask.io' + ? 'differentNetworkClientId' + : 'selectedNetworkClientId', + ), + }); + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(messenger), + }; + const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, ); + + await expect(firstRequest).rejects.toThrow(switchError); + await secondRequest; + + expect(secondRequestNext).toHaveBeenCalled(); }); + }); - it('rejects all requests that require a switch if the selected network network configuration is missing', async () => { - const mockSetActiveNetwork = jest.fn(); + describe('when the network switch for a batch fails', () => { + it('throws error', async () => { + const switchError = new Error('switch error'); const { messenger } = buildControllerMessenger({ networkControllerGetState: jest.fn().mockReturnValue({ ...cloneDeep(defaultNetworkState), selectedNetworkClientId: 'selectedNetworkClientId', }), - networkControllerGetNetworkConfigurationByNetworkClientId: ( - networkClientId, - ) => - networkClientId === 'differentNetworkClientId' - ? { chainId: '0x999', rpcUrl: 'metamask.io', ticker: 'TEST' } - : undefined, - networkControllerSetActiveNetwork: mockSetActiveNetwork, + networkControllerSetActiveNetwork: jest + .fn() + .mockRejectedValue(switchError), + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((origin) => + origin === 'https://secondorigin.metamask.io' + ? 'differentNetworkClientId' + : 'selectedNetworkClientId', + ), }); const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(messenger), }; const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + // ensure test starts with one request queued up + expect(controller.state.queuedRequestCount).toBe(1); + expect(secondRequestNext).not.toHaveBeenCalled(); + await firstRequest; + await expect(secondRequest).rejects.toThrow(switchError); + }); + + it('correctly processes the next item in the queue', async () => { + const switchError = new Error('switch error'); + const { messenger } = buildControllerMessenger({ + networkControllerGetState: jest.fn().mockReturnValue({ + ...cloneDeep(defaultNetworkState), + selectedNetworkClientId: 'selectedNetworkClientId', + }), + networkControllerSetActiveNetwork: jest + .fn() + .mockRejectedValue(switchError), + selectedNetworkControllerGetNetworkClientIdForDomain: jest + .fn() + .mockImplementation((origin) => + origin === 'https://secondorigin.metamask.io' + ? 'differentNetworkClientId' + : 'selectedNetworkClientId', + ), + }); + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(messenger), + }; + const controller = new QueuedRequestController(options); + const firstRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://firstorigin.metamask.io' }, + () => new Promise((resolve) => setTimeout(resolve, 10)), + ); + // ensure first request skips queue + expect(controller.state.queuedRequestCount).toBe(0); + const secondRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const secondRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://secondorigin.metamask.io' }, + secondRequestNext, + ); + const thirdRequestNext = jest + .fn() + .mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 100)), + ); + const thirdRequest = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://thirdorigin.metamask.io' }, + thirdRequestNext, + ); + // ensure test starts with two requests queued up + expect(controller.state.queuedRequestCount).toBe(2); + expect(secondRequestNext).not.toHaveBeenCalled(); + + await firstRequest; + await expect(secondRequest).rejects.toThrow(switchError); + await thirdRequest; + + expect(thirdRequestNext).toHaveBeenCalled(); + }); + }); + + describe('when a request fails', () => { + it('throws error', async () => { + const options: QueuedRequestControllerOptions = { + messenger: buildQueuedRequestControllerMessenger(), + }; + + const controller = new QueuedRequestController(options); + + // Mock a request that throws an error + const requestWithError = jest.fn(() => + Promise.reject(new Error('Request failed')), + ); + + // Enqueue the request await expect(() => controller.enqueueRequest( - { - ...buildRequest(), - networkClientId: 'differentNetworkClientId', - }, - () => new Promise((resolve) => setTimeout(resolve, 10)), + { ...buildRequest(), origin: 'example.metamask.io' }, + requestWithError, ), - ).rejects.toThrow( - 'Missing network configuration for selectedNetworkClientId', - ); + ).rejects.toThrow(new Error('Request failed')); + expect(controller.state.queuedRequestCount).toBe(0); }); it('correctly updates the request queue count upon failure', async () => { @@ -294,14 +719,17 @@ describe('QueuedRequestController', () => { const controller = new QueuedRequestController(options); await expect(() => - controller.enqueueRequest(buildRequest(), async () => { - throw new Error('Request failed'); - }), + controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example.metamask.io' }, + async () => { + throw new Error('Request failed'); + }, + ), ).rejects.toThrow('Request failed'); expect(controller.state.queuedRequestCount).toBe(0); }); - it('handles errors without interrupting the execution of the next item in the queue', async () => { + it('correctly processes the next item in the queue', async () => { const options: QueuedRequestControllerOptions = { messenger: buildQueuedRequestControllerMessenger(), }; @@ -322,9 +750,18 @@ describe('QueuedRequestController', () => { }); // Enqueue the requests - const promise1 = controller.enqueueRequest(buildRequest(), request1); - const promise2 = controller.enqueueRequest(buildRequest(), request2); - const promise3 = controller.enqueueRequest(buildRequest(), request3); + const promise1 = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example1.metamask.io' }, + request1, + ); + const promise2 = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example2.metamask.io' }, + request2, + ); + const promise3 = controller.enqueueRequest( + { ...buildRequest(), origin: 'https://example3.metamask.io' }, + request3, + ); expect( await Promise.allSettled([promise1, promise2, promise3]), @@ -345,55 +782,43 @@ describe('QueuedRequestController', () => { * Build a controller messenger setup with QueuedRequestController types. * * @param options - Options - * @param options.networkControllerGetNetworkConfigurationByNetworkClientId - A handler for the - * `NetworkController:getNetworkConfigurationByNetworkClientId` action. * @param options.networkControllerGetState - A handler for the `NetworkController:getState` * action. * @param options.networkControllerSetActiveNetwork - A handler for the * `NetworkController:setActiveNetwork` action. - * @param options.approvalControllerAddRequest - A handler for the `ApprovalController:addRequest` - * action. + * @param options.selectedNetworkControllerGetNetworkClientIdForDomain - A handler for the + * `SelectedNetworkController:getNetworkClientIdForDomain` action. * @returns A controller messenger with QueuedRequestController types, and * mocks for all allowed actions. */ function buildControllerMessenger({ - networkControllerGetNetworkConfigurationByNetworkClientId, networkControllerGetState, networkControllerSetActiveNetwork, - approvalControllerAddRequest, + selectedNetworkControllerGetNetworkClientIdForDomain, }: { - networkControllerGetNetworkConfigurationByNetworkClientId?: NetworkControllerGetNetworkConfigurationByNetworkClientId['handler']; networkControllerGetState?: NetworkControllerGetStateAction['handler']; networkControllerSetActiveNetwork?: NetworkControllerSetActiveNetworkAction['handler']; - approvalControllerAddRequest?: AddApprovalRequest['handler']; + selectedNetworkControllerGetNetworkClientIdForDomain?: SelectedNetworkControllerGetNetworkClientIdForDomainAction['handler']; } = {}): { messenger: ControllerMessenger< QueuedRequestControllerActions | AllowedActions, QueuedRequestControllerEvents >; - mockNetworkControllerGetNetworkConfigurationByNetworkClientId: jest.Mocked< - NetworkControllerGetNetworkConfigurationByNetworkClientId['handler'] - >; mockNetworkControllerGetState: jest.Mocked< NetworkControllerGetStateAction['handler'] >; mockNetworkControllerSetActiveNetwork: jest.Mocked< NetworkControllerSetActiveNetworkAction['handler'] >; - mockApprovalControllerAddRequest: jest.Mocked; + mockSelectedNetworkControllerGetNetworkClientIdForDomain: jest.Mocked< + SelectedNetworkControllerGetNetworkClientIdForDomainAction['handler'] + >; } { const messenger = new ControllerMessenger< QueuedRequestControllerActions | AllowedActions, QueuedRequestControllerEvents >(); - const mockNetworkControllerGetNetworkConfigurationByNetworkClientId = - networkControllerGetNetworkConfigurationByNetworkClientId ?? - jest.fn().mockReturnValue({}); - messenger.registerActionHandler( - 'NetworkController:getNetworkConfigurationByNetworkClientId', - mockNetworkControllerGetNetworkConfigurationByNetworkClientId, - ); const mockNetworkControllerGetState = networkControllerGetState ?? jest.fn().mockReturnValue({ @@ -410,18 +835,17 @@ function buildControllerMessenger({ 'NetworkController:setActiveNetwork', mockNetworkControllerSetActiveNetwork, ); - const mockApprovalControllerAddRequest = - approvalControllerAddRequest ?? jest.fn(); + const mockSelectedNetworkControllerGetNetworkClientIdForDomain = + selectedNetworkControllerGetNetworkClientIdForDomain ?? jest.fn(); messenger.registerActionHandler( - 'ApprovalController:addRequest', - mockApprovalControllerAddRequest, + 'SelectedNetworkController:getNetworkClientIdForDomain', + mockSelectedNetworkControllerGetNetworkClientIdForDomain, ); return { messenger, - mockNetworkControllerGetNetworkConfigurationByNetworkClientId, mockNetworkControllerGetState, mockNetworkControllerSetActiveNetwork, - mockApprovalControllerAddRequest, + mockSelectedNetworkControllerGetNetworkClientIdForDomain, }; } @@ -439,8 +863,7 @@ function buildQueuedRequestControllerMessenger( allowedActions: [ 'NetworkController:getState', 'NetworkController:setActiveNetwork', - 'NetworkController:getNetworkConfigurationByNetworkClientId', - 'ApprovalController:addRequest', + 'SelectedNetworkController:getNetworkClientIdForDomain', ], }); } @@ -455,7 +878,7 @@ function buildRequest(): QueuedRequestMiddlewareJsonRpcRequest { method: 'doesnt matter', id: 'doesnt matter', jsonrpc: '2.0' as const, - origin: 'example.com', + origin: 'example.metamask.io', networkClientId: 'mainnet', }; } diff --git a/packages/queued-request-controller/src/QueuedRequestController.ts b/packages/queued-request-controller/src/QueuedRequestController.ts index e2085bbff4..f0060d75a0 100644 --- a/packages/queued-request-controller/src/QueuedRequestController.ts +++ b/packages/queued-request-controller/src/QueuedRequestController.ts @@ -1,16 +1,15 @@ -import type { AddApprovalRequest } from '@metamask/approval-controller'; import type { ControllerGetStateAction, ControllerStateChangeEvent, RestrictedControllerMessenger, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import { ApprovalType } from '@metamask/controller-utils'; import type { - NetworkControllerGetNetworkConfigurationByNetworkClientId, NetworkControllerGetStateAction, NetworkControllerSetActiveNetworkAction, } from '@metamask/network-controller'; +import type { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller'; +import { createDeferredPromise } from '@metamask/utils'; import type { QueuedRequestMiddlewareJsonRpcRequest } from './types'; @@ -36,6 +35,7 @@ export type QueuedRequestControllerEnqueueRequestAction = { }; export const QueuedRequestControllerEventTypes = { + networkSwitched: `${controllerName}:networkSwitched` as const, stateChange: `${controllerName}:stateChange` as const, }; @@ -45,8 +45,14 @@ export type QueuedRequestControllerStateChangeEvent = QueuedRequestControllerState >; +export type QueuedRequestControllerNetworkSwitched = { + type: typeof QueuedRequestControllerEventTypes.networkSwitched; + payload: [string]; +}; + export type QueuedRequestControllerEvents = - QueuedRequestControllerStateChangeEvent; + | QueuedRequestControllerStateChangeEvent + | QueuedRequestControllerNetworkSwitched; export type QueuedRequestControllerActions = | QueuedRequestControllerGetStateAction @@ -55,8 +61,7 @@ export type QueuedRequestControllerActions = export type AllowedActions = | NetworkControllerGetStateAction | NetworkControllerSetActiveNetworkAction - | NetworkControllerGetNetworkConfigurationByNetworkClientId - | AddApprovalRequest; + | SelectedNetworkControllerGetNetworkClientIdForDomainAction; export type QueuedRequestControllerMessenger = RestrictedControllerMessenger< typeof controllerName, @@ -71,28 +76,62 @@ export type QueuedRequestControllerOptions = { }; /** - * Controller for request queueing. The QueuedRequestController manages the orderly execution of enqueued requests - * to prevent concurrency issues and ensure proper handling of asynchronous operations. + * A queued request. + */ +type QueuedRequest = { + /** + * The origin of the queued request. + */ + origin: string; + /** + * A callback used to continue processing the request, called when the request is dequeued. + */ + processRequest: (error: unknown) => void; +}; + +/** + * Queue requests for processing in batches, by request origin. * - * @param options - The controller options, including the restricted controller messenger for the QueuedRequestController. - * @param options.messenger - The restricted controller messenger that facilitates communication with the QueuedRequestController. + * Processing requests in batches allows us to completely separate sets of requests that originate + * from different origins. This ensures that our UI will not display those requests as a set, which + * could mislead users into thinking they are related. * - * The QueuedRequestController maintains a count of enqueued requests, allowing you to monitor the queue's workload. - * It processes requests sequentially, ensuring that each request is executed one after the other. The class offers - * an `enqueueRequest` method for adding requests to the queue. The controller initializes with a count of zero and - * registers message handlers for request enqueuing. It also publishes count changes to inform external observers. + * Queuing requests in batches also allows us to ensure the globally selected network matches the + * dapp-selected network, before the confirmation UI is rendered. This is important because the + * data shown on some confirmation screens is only collected for the globally selected network. + * + * Requests get processed in order of insertion, even across batches. All requests get processed + * even in the event of preceding requests failing. */ export class QueuedRequestController extends BaseController< typeof controllerName, QueuedRequestControllerState, QueuedRequestControllerMessenger > { - private currentRequest: Promise = Promise.resolve(); + /** + * The origin of the current batch of requests being processed, or `undefined` if there are no + * requests currently being processed. + */ + #originOfCurrentBatch: string | undefined; /** - * Constructs a QueuedRequestController, responsible for managing and processing enqueued requests sequentially. - * @param options - The controller options, including the restricted controller messenger for the QueuedRequestController. - * @param options.messenger - The restricted controller messenger that facilitates communication with the QueuedRequestController. + * The list of all queued requests, in chronological order. + */ + #requestQueue: QueuedRequest[] = []; + + /** + * The number of requests currently being processed. + * + * Note that this does not include queued requests, just those being actively processed (i.e. + * those in the "current batch"). + */ + #processingRequestCount = 0; + + /** + * Construct a QueuedRequestController. + * + * @param options - Controller options. + * @param options.messenger - The restricted controller messenger that facilitates communication with other controllers. */ constructor({ messenger }: QueuedRequestControllerOptions) { super({ @@ -111,118 +150,160 @@ export class QueuedRequestController extends BaseController< #registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( - QueuedRequestControllerActionTypes.enqueueRequest, + `${controllerName}:enqueueRequest`, this.enqueueRequest.bind(this), ); } /** - * Switch the current globally selected network if necessary for processing the given - * request. + * Process the next batch of requests. + * + * This will trigger the next batch of requests with matching origins to be processed. Each + * request in the batch is dequeued one at a time, in chronological order, but they all get + * processed in parallel. + * + * This should be called after a batch of requests has finished processing, if the queue is non- + * empty. + */ + async #processNextBatch() { + const firstRequest = this.#requestQueue.shift() as QueuedRequest; + this.#originOfCurrentBatch = firstRequest.origin; + const batch = [firstRequest.processRequest]; + while (this.#requestQueue[0]?.origin === this.#originOfCurrentBatch) { + const nextEntry = this.#requestQueue.shift() as QueuedRequest; + batch.push(nextEntry.processRequest); + } + + // If globally selected network is different from origin selected network, + // switch network before processing batch + let networkSwitchError: unknown; + try { + await this.#switchNetworkIfNecessary(); + } catch (error: unknown) { + networkSwitchError = error; + } + + for (const processRequest of batch) { + processRequest(networkSwitchError); + } + this.#updateQueuedRequestCount(); + } + + /** + * Switch the globally selected network client to match the network + * client of the current batch. * - * @param request - The request currently being processed. * @throws Throws an error if the current selected `networkClientId` or the * `networkClientId` on the request are invalid. */ - async #switchNetworkIfNecessary( - request: QueuedRequestMiddlewareJsonRpcRequest, - ) { + async #switchNetworkIfNecessary() { + // This branch is unreachable; it's just here for type reasons. + /* istanbul ignore next */ + if (!this.#originOfCurrentBatch) { + throw new Error('Current batch origin must be initialized first'); + } + const originNetworkClientId = this.messagingSystem.call( + 'SelectedNetworkController:getNetworkClientIdForDomain', + this.#originOfCurrentBatch, + ); const { selectedNetworkClientId } = this.messagingSystem.call( 'NetworkController:getState', ); - if (request.networkClientId === selectedNetworkClientId) { + if (originNetworkClientId === selectedNetworkClientId) { return; } - const toNetworkConfiguration = this.messagingSystem.call( - 'NetworkController:getNetworkConfigurationByNetworkClientId', - request.networkClientId, - ); - const fromNetworkConfiguration = this.messagingSystem.call( - 'NetworkController:getNetworkConfigurationByNetworkClientId', - selectedNetworkClientId, - ); - if (!toNetworkConfiguration) { - throw new Error( - `Missing network configuration for ${request.networkClientId}`, - ); - } else if (!fromNetworkConfiguration) { - throw new Error( - `Missing network configuration for ${selectedNetworkClientId}`, - ); - } - - const requestData = { - toNetworkConfiguration, - fromNetworkConfiguration, - }; await this.messagingSystem.call( - 'ApprovalController:addRequest', - { - origin: request.origin, - type: ApprovalType.SwitchEthereumChain, - requestData, - }, - true, + 'NetworkController:setActiveNetwork', + originNetworkClientId, ); - await this.messagingSystem.call( - 'NetworkController:setActiveNetwork', - request.networkClientId, + this.messagingSystem.publish( + 'QueuedRequestController:networkSwitched', + originNetworkClientId, ); } - #updateCount(change: -1 | 1) { + /** + * Update the queued request count. + */ + #updateQueuedRequestCount() { this.update((state) => { - state.queuedRequestCount += change; + state.queuedRequestCount = this.#requestQueue.length; }); } /** - * Enqueues a new request for sequential processing in the request queue. This function manages the order of - * requests, ensuring they are executed one after the other to prevent concurrency issues and maintain proper - * execution flow. + * Enqueue a request to be processed in a batch with other requests from the same origin. + * + * We process requests one origin at a time, so that requests from different origins do not get + * interwoven, and so that we can ensure that the globally selected network matches the dapp- + * selected network. + * + * Requests get processed in order of insertion, even across origins/batches. All requests get + * processed even in the event of preceding requests failing. * * @param request - The JSON-RPC request to process. - * @param requestNext - A function representing the next steps for processing this request. It returns a promise that - * resolves when the request is complete. - * @returns A promise that resolves when the enqueued request and any subsequent asynchronous - * operations are fully processed. This allows you to await the completion of the enqueued request before continuing - * with additional actions. If there are multiple enqueued requests, this function ensures they are processed in - * the order they were enqueued, guaranteeing sequential execution. + * @param requestNext - A function representing the next steps for processing this request. + * @returns A promise that resolves when the given request has been fully processed. */ async enqueueRequest( request: QueuedRequestMiddlewareJsonRpcRequest, requestNext: () => Promise, - ) { - this.#updateCount(1); - if (this.state.queuedRequestCount > 1) { - try { - await this.currentRequest; - } catch (_error) { - // error ignored - this is handled in the middleware instead - this.#updateCount(-1); - } + ): Promise { + if (this.#originOfCurrentBatch === undefined) { + this.#originOfCurrentBatch = request.origin; } - const processCurrentRequest = async () => { - try { - if ( - request.method !== 'wallet_switchEthereumChain' && - request.method !== 'wallet_addEthereumChain' - ) { - await this.#switchNetworkIfNecessary(request); - } + try { + // Queue request for later processing + // Network switch is handled when this batch is processed + if ( + this.state.queuedRequestCount > 0 || + this.#originOfCurrentBatch !== request.origin + ) { + const { + promise: waitForDequeue, + reject, + resolve, + } = createDeferredPromise({ + suppressUnhandledRejection: true, + }); + this.#requestQueue.push({ + origin: request.origin, + processRequest: (error: unknown) => { + if (error) { + reject(error); + } else { + resolve(); + } + }, + }); + this.#updateQueuedRequestCount(); + await waitForDequeue; + } else { + // Process request immediately + // Requires switching network now if necessary + await this.#switchNetworkIfNecessary(); + } + this.#processingRequestCount += 1; + try { await requestNext(); } finally { - // The count is updated as part of the request processing to ensure - // that it has been updated before the next request is run. - this.#updateCount(-1); + this.#processingRequestCount -= 1; } - }; - - this.currentRequest = processCurrentRequest(); - await this.currentRequest; + return undefined; + } finally { + if (this.#processingRequestCount === 0) { + this.#originOfCurrentBatch = undefined; + if (this.#requestQueue.length > 0) { + // The next batch is triggered here. We intentionally omit the `await` because we don't + // want the next batch to block resolution of the current request. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.#processNextBatch(); + } + } + } } } diff --git a/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts b/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts index d5f399c86e..f8b1b5dead 100644 --- a/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts +++ b/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts @@ -28,7 +28,7 @@ const getMockEnqueueRequest = () => ReturnType, Parameters >() - .mockImplementation((_origin, requestNext) => requestNext()); + .mockImplementation((_request, requestNext) => requestNext()); describe('createQueuedRequestMiddleware', () => { it('throws if not provided an origin', async () => { diff --git a/packages/queued-request-controller/src/index.ts b/packages/queued-request-controller/src/index.ts index 0461d5694d..5fc9509ce1 100644 --- a/packages/queued-request-controller/src/index.ts +++ b/packages/queued-request-controller/src/index.ts @@ -3,6 +3,7 @@ export type { QueuedRequestControllerEnqueueRequestAction, QueuedRequestControllerGetStateAction, QueuedRequestControllerStateChangeEvent, + QueuedRequestControllerNetworkSwitched, QueuedRequestControllerEvents, QueuedRequestControllerActions, QueuedRequestControllerMessenger, diff --git a/packages/queued-request-controller/tsconfig.build.json b/packages/queued-request-controller/tsconfig.build.json index f4019a1ea6..8f665a6ac3 100644 --- a/packages/queued-request-controller/tsconfig.build.json +++ b/packages/queued-request-controller/tsconfig.build.json @@ -8,7 +8,6 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../approval-controller/tsconfig.build.json" }, { "path": "../selected-network-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../json-rpc-engine/tsconfig.build.json" } diff --git a/packages/queued-request-controller/tsconfig.json b/packages/queued-request-controller/tsconfig.json index b1de426b26..4765dda816 100644 --- a/packages/queued-request-controller/tsconfig.json +++ b/packages/queued-request-controller/tsconfig.json @@ -11,9 +11,6 @@ { "path": "../network-controller" }, - { - "path": "../approval-controller" - }, { "path": "../selected-network-controller" }, diff --git a/yarn.lock b/yarn.lock index b7a0d9964e..0ebea0f079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2746,7 +2746,6 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" dependencies: - "@metamask/approval-controller": ^5.1.3 "@metamask/auto-changelog": ^3.4.4 "@metamask/base-controller": ^4.1.1 "@metamask/controller-utils": ^8.0.4 @@ -2768,7 +2767,6 @@ __metadata: typedoc-plugin-missing-exports: ^2.0.0 typescript: ~4.8.4 peerDependencies: - "@metamask/approval-controller": ^5.1.2 "@metamask/network-controller": ^17.2.0 "@metamask/selected-network-controller": ^9.0.0 languageName: unknown