diff --git a/README.md b/README.md index 48d06d72c30..f835529429a 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ linkStyle default opacity:0.5 name_controller --> controller_utils; name_controller --> messenger; network_controller --> base_controller; + network_controller --> connectivity_controller; network_controller --> controller_utils; network_controller --> eth_block_tracker; network_controller --> eth_json_rpc_middleware; diff --git a/packages/assets-controllers/src/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts index 96af8484b2f..89e14a370bc 100644 --- a/packages/assets-controllers/src/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -113,6 +113,7 @@ async function setupAssetContractControllers({ getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), }); if (useNetworkControllerProvider) { diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index 1d4f5e2e2ea..75685ef8329 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -102,6 +102,7 @@ const setupNetworkController = async ({ getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), }); diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 5817ea03e03..660479382f8 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** NetworkController now requires `ConnectivityController:getState` action handler to be registered on the messenger ([#7627](https://github.com/MetaMask/core/pull/7627)) + - The `NetworkController` now depends on the `ConnectivityController` to prevent retries and suppress events when the user is offline. + - When offline, `NetworkController:rpcEndpointUnavailable` and `NetworkController:rpcEndpointDegraded` events are suppressed since retries don't occur and circuit breakers don't trigger. + - You must register a `ConnectivityController:getState` action handler on your root messenger that returns an object with a `connectivityStatus` property (`'online'` or `'offline'`). + - You must delegate the `ConnectivityController:getState` action from your root messenger to the `NetworkControllerMessenger` using `rootMessenger.delegate({ messenger: networkControllerMessenger, actions: ['ConnectivityController:getState'] })`. + ## [28.0.0] ### Changed diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 1fc7176dee7..1834430ebda 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.0.0", + "@metamask/connectivity-controller": "^0.0.0", "@metamask/controller-utils": "^11.18.0", "@metamask/eth-block-tracker": "^15.0.0", "@metamask/eth-json-rpc-infura": "^10.3.0", diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 1652fa011a1..87e0603368c 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -3,6 +3,7 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import type { ConnectivityControllerGetStateAction } from '@metamask/connectivity-controller'; import type { Partialize } from '@metamask/controller-utils'; import { InfuraNetworkType, @@ -729,7 +730,7 @@ export type NetworkControllerActions = /** * All actions that {@link NetworkController} calls internally. */ -type AllowedActions = never; +type AllowedActions = ConnectivityControllerGetStateAction; export type NetworkControllerMessenger = Messenger< typeof controllerName, diff --git a/packages/network-controller/src/create-auto-managed-network-client.test.ts b/packages/network-controller/src/create-auto-managed-network-client.test.ts index 9ef882d9d9d..07927cbbcee 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.test.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.test.ts @@ -42,6 +42,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -59,6 +60,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -73,6 +75,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -107,6 +110,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -147,6 +151,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -211,6 +216,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -285,6 +291,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -341,6 +348,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -401,6 +409,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, @@ -464,6 +473,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -524,6 +534,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -592,6 +603,7 @@ describe('createAutoManagedNetworkClient', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, }); const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ pollingInterval: 5000, @@ -657,6 +669,7 @@ describe('createAutoManagedNetworkClient', () => { getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, diff --git a/packages/network-controller/src/create-network-client-tests/rpc-endpoint-events.test.ts b/packages/network-controller/src/create-network-client-tests/rpc-endpoint-events.test.ts index b3352b777c5..a4f9cfdbb65 100644 --- a/packages/network-controller/src/create-network-client-tests/rpc-endpoint-events.test.ts +++ b/packages/network-controller/src/create-network-client-tests/rpc-endpoint-events.test.ts @@ -1,3 +1,4 @@ +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; import { ConstantBackoff, DEFAULT_DEGRADED_THRESHOLD, @@ -82,6 +83,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -198,6 +200,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId, rpcUrl }) => { @@ -263,6 +266,182 @@ describe('createNetworkClient - RPC endpoint events', () => { ); }); + it('does not retry requests when user is offline', async () => { + const failoverEndpointUrl = 'https://failover.endpoint/'; + const request = { + method: 'eth_gasPrice', + params: [], + }; + const expectedError = createResourceUnavailableError(503); + + await withMockedCommunications( + { providerType: networkClientType }, + async (primaryComms) => { + await withMockedCommunications( + { + providerType: 'custom', + customRpcUrl: failoverEndpointUrl, + }, + async () => { + // Mock only one failure - if retries were happening, we'd need more + primaryComms.mockRpcCall({ + request: { + method: 'eth_blockNumber', + params: [], + }, + times: 1, + response: { + httpStatus: 503, + }, + }); + + const rootMessenger = buildRootMessenger({ + connectivityStatus: CONNECTIVITY_STATUSES.Offline, + }); + + const rpcEndpointRetriedEventHandler = jest.fn(); + rootMessenger.subscribe( + 'NetworkController:rpcEndpointRetried', + rpcEndpointRetriedEventHandler, + ); + + await withNetworkClient( + { + providerType: networkClientType, + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + isRpcFailoverEnabled: true, + failoverRpcUrls: [failoverEndpointUrl], + messenger: rootMessenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + policyOptions: { + backoff: new ConstantBackoff(backoffDuration), + }, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall }) => { + // When offline, errors are not retried, so the request + // should fail immediately without retries + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + + // Verify that retry event was not published + expect( + rpcEndpointRetriedEventHandler, + ).not.toHaveBeenCalled(); + }, + ); + }, + ); + }, + ); + }); + + it('suppresses the NetworkController:rpcEndpointUnavailable event when user is offline', async () => { + const failoverEndpointUrl = 'https://failover.endpoint/'; + const request = { + method: 'eth_gasPrice', + params: [], + }; + const expectedError = createResourceUnavailableError(503); + + await withMockedCommunications( + { providerType: networkClientType }, + async (primaryComms) => { + await withMockedCommunications( + { + providerType: 'custom', + customRpcUrl: failoverEndpointUrl, + }, + async (failoverComms) => { + // The first time a block-cacheable request is made, the + // latest block number is retrieved through the block + // tracker first. + primaryComms.mockRpcCall({ + request: { + method: 'eth_blockNumber', + params: [], + }, + times: DEFAULT_MAX_CONSECUTIVE_FAILURES, + response: { + httpStatus: 503, + }, + }); + failoverComms.mockRpcCall({ + request: { + method: 'eth_blockNumber', + params: [], + }, + times: DEFAULT_MAX_CONSECUTIVE_FAILURES, + response: { + httpStatus: 503, + }, + }); + + const rootMessenger = buildRootMessenger({ + connectivityStatus: CONNECTIVITY_STATUSES.Offline, + }); + + const rpcEndpointUnavailableEventHandler = jest.fn(); + rootMessenger.subscribe( + 'NetworkController:rpcEndpointUnavailable', + rpcEndpointUnavailableEventHandler, + ); + + await withNetworkClient( + { + providerType: networkClientType, + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + isRpcFailoverEnabled: true, + failoverRpcUrls: [failoverEndpointUrl], + messenger: rootMessenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + policyOptions: { + backoff: new ConstantBackoff(backoffDuration), + }, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall, clock }) => { + rootMessenger.subscribe( + 'NetworkController:rpcEndpointRetried', + () => { + // Ensure that we advance to the next RPC request + // retry, not the next block tracker request. + clock.tick(backoffDuration); + }, + ); + + // When offline, errors are not retried, so the circuit + // won't break and onServiceBreak won't be called + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + + // Event should be suppressed when offline because retries + // are prevented, so onServiceBreak is never called + expect( + rpcEndpointUnavailableEventHandler, + ).not.toHaveBeenCalled(); + }, + ); + }, + ); + }, + ); + }); + it('does not publish the NetworkController:rpcEndpointChainDegraded event again if the max number of retries is reached in making requests to a failover endpoint', async () => { const failoverEndpointUrl = 'https://failover.endpoint/'; const request = { @@ -301,6 +480,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -408,6 +588,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -523,6 +704,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId, rpcUrl }) => { @@ -650,6 +832,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId, rpcUrl }) => { @@ -822,6 +1005,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -908,6 +1092,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -977,6 +1162,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId }) => { @@ -1068,6 +1254,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId, rpcUrl }) => { @@ -1117,6 +1304,158 @@ describe('createNetworkClient - RPC endpoint events', () => { ); }); + it('does not retry requests when user is offline (degraded scenario)', async () => { + const request = { + method: 'eth_gasPrice', + params: [], + }; + const expectedError = createResourceUnavailableError(503); + + await withMockedCommunications( + { providerType: networkClientType }, + async (comms) => { + // Mock only one failure - if retries were happening, we'd need more + comms.mockRpcCall({ + request: { + method: 'eth_blockNumber', + params: [], + }, + times: 1, + response: { + httpStatus: 503, + }, + }); + comms.mockRpcCall({ + request: { + method: 'eth_gasPrice', + params: [], + }, + times: 1, + response: { + httpStatus: 503, + }, + }); + + const rootMessenger = buildRootMessenger({ + connectivityStatus: CONNECTIVITY_STATUSES.Offline, + }); + + const rpcEndpointRetriedEventHandler = jest.fn(); + rootMessenger.subscribe( + 'NetworkController:rpcEndpointRetried', + rpcEndpointRetriedEventHandler, + ); + + await withNetworkClient( + { + providerType: networkClientType, + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + messenger: rootMessenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + policyOptions: { + backoff: new ConstantBackoff(backoffDuration), + }, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall }) => { + // When offline, errors are not retried, so the request + // should fail immediately without retries + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + + // Verify that retry event was not published + expect(rpcEndpointRetriedEventHandler).not.toHaveBeenCalled(); + }, + ); + }, + ); + }); + + it('suppresses the NetworkController:rpcEndpointDegraded event when user is offline', async () => { + const request = { + method: 'eth_gasPrice', + params: [], + }; + const expectedError = createResourceUnavailableError(503); + + await withMockedCommunications( + { providerType: networkClientType }, + async (comms) => { + // The first time a block-cacheable request is made, the + // latest block number is retrieved through the block + // tracker first. + comms.mockRpcCall({ + request: { + method: 'eth_blockNumber', + params: [], + }, + times: DEFAULT_MAX_CONSECUTIVE_FAILURES, + response: { + httpStatus: 503, + }, + }); + + const rootMessenger = buildRootMessenger({ + connectivityStatus: CONNECTIVITY_STATUSES.Offline, + }); + + const rpcEndpointDegradedEventHandler = jest.fn(); + rootMessenger.subscribe( + 'NetworkController:rpcEndpointDegraded', + rpcEndpointDegradedEventHandler, + ); + + await withNetworkClient( + { + providerType: networkClientType, + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + messenger: rootMessenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + policyOptions: { + backoff: new ConstantBackoff(backoffDuration), + }, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall, clock }) => { + rootMessenger.subscribe( + 'NetworkController:rpcEndpointRetried', + () => { + // Ensure that we advance to the next RPC request + // retry, not the next block tracker request. + clock.tick(backoffDuration); + }, + ); + + // When offline, errors are not retried, so the circuit + // won't accumulate failures and onServiceDegraded won't be called + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + await expect(makeRpcCall(request)).rejects.toThrow( + expectedError, + ); + + // Event should be suppressed when offline because retries + // are prevented, so onServiceDegraded is never called + expect( + rpcEndpointDegradedEventHandler, + ).not.toHaveBeenCalled(); + }, + ); + }, + ); + }); + it('publishes the NetworkController:rpcEndpointDegraded event when the time to complete a request to a primary endpoint is continually too long', async () => { const request = { method: 'eth_gasPrice', @@ -1147,6 +1486,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, clock, chainId, rpcUrl }) => { @@ -1240,6 +1580,7 @@ describe('createNetworkClient - RPC endpoint events', () => { policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, + isOffline: (): boolean => false, }), }, async ({ makeRpcCall, chainId }) => { diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index e06d853b084..6a0a78c9148 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -1,3 +1,4 @@ +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; import type { CockatielFailureReason, InfuraNetworkType, @@ -210,10 +211,19 @@ function createRpcServiceChain({ const availableEndpointUrls: [string, ...string[]] = isRpcFailoverEnabled ? [primaryEndpointUrl, ...(configuration.failoverRpcUrls ?? [])] : [primaryEndpointUrl]; + + const isOffline = (): boolean => { + const connectivityState = messenger.call('ConnectivityController:getState'); + return ( + connectivityState.connectivityStatus === CONNECTIVITY_STATUSES.Offline + ); + }; + const rpcServiceConfigurations = availableEndpointUrls.map((endpointUrl) => ({ ...getRpcServiceOptions(endpointUrl), endpointUrl, logger, + isOffline, })); /** @@ -305,6 +315,7 @@ function createRpcServiceChain({ ...rest }) => { const error = getError(rest); + messenger.publish('NetworkController:rpcEndpointDegraded', { chainId: configuration.chainId, networkClientId: id, diff --git a/packages/network-controller/src/rpc-service/rpc-service-chain.test.ts b/packages/network-controller/src/rpc-service/rpc-service-chain.test.ts index c6a6f106efa..6e1508a6e0a 100644 --- a/packages/network-controller/src/rpc-service/rpc-service-chain.test.ts +++ b/packages/network-controller/src/rpc-service/rpc-service-chain.test.ts @@ -56,6 +56,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -73,6 +74,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -90,6 +92,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -107,6 +110,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -124,6 +128,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -143,6 +148,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://rpc.example.chain', }, ]); @@ -173,11 +179,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://first.endpoint', }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://second.endpoint', fetchOptions: { headers: { @@ -188,6 +196,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://third.chain', }, ]); @@ -242,16 +251,19 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://first.endpoint', }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://second.endpoint', }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://third.chain', }, ]); @@ -345,11 +357,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://first.endpoint', }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://second.endpoint', fetchOptions: { headers: { @@ -360,6 +374,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: 'https://third.chain', fetchOptions: { referrer: 'https://some.referrer', @@ -427,6 +442,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -501,11 +517,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, fetchOptions: { headers: { @@ -516,6 +534,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: tertiaryEndpointUrl, }, ]); @@ -593,11 +612,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -657,11 +678,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -756,11 +779,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -856,11 +881,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -940,16 +967,19 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: tertiaryEndpointUrl, }, ]); @@ -1034,6 +1064,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1091,6 +1122,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1154,6 +1186,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1219,11 +1252,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -1292,6 +1327,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1374,11 +1410,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -1445,6 +1483,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1509,6 +1548,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1579,6 +1619,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1655,11 +1696,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -1739,6 +1782,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1831,11 +1875,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -1924,6 +1970,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); @@ -1976,11 +2023,13 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: primaryEndpointUrl, }, { fetch, btoa, + isOffline: (): boolean => false, endpointUrl: secondaryEndpointUrl, }, ]); @@ -2046,6 +2095,7 @@ describe('RpcServiceChain', () => { { fetch, btoa, + isOffline: (): boolean => false, endpointUrl, }, ]); diff --git a/packages/network-controller/src/rpc-service/rpc-service.test.ts b/packages/network-controller/src/rpc-service/rpc-service.test.ts index 463c80f93e9..18edeb6c867 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.test.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.test.ts @@ -56,6 +56,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -106,6 +107,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -172,6 +174,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -225,6 +228,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -273,6 +277,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -320,6 +325,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -453,6 +459,7 @@ describe('RpcService', () => { }, btoa, endpointUrl, + isOffline: (): boolean => false, }); }, expectedError: new Error('oops'), @@ -594,6 +601,109 @@ describe('RpcService', () => { }, ); + describe('when offline', () => { + it('does not retry when offline, only makes one fetch call', async () => { + const expectedError = new TypeError('Failed to fetch'); + const mockFetch = jest.fn(() => { + throw expectedError; + }); + const service = new RpcService({ + fetch: mockFetch, + btoa, + endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => true, + }); + service.onRetry(() => { + clock.next(); + }); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + // When offline, no retries should happen, so only 1 fetch call + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it('does not call onDegraded when offline', async () => { + const expectedError = new TypeError('Failed to fetch'); + const mockFetch = jest.fn(() => { + throw expectedError; + }); + const endpointUrl = 'https://rpc.example.chain'; + const onDegradedListener = jest.fn(); + const service = new RpcService({ + fetch: mockFetch, + btoa, + endpointUrl, + isOffline: (): boolean => true, + }); + service.onRetry(() => { + clock.next(); + }); + service.onDegraded(onDegradedListener); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + + // When offline, retries don't happen, so onDegraded should not be called + expect(onDegradedListener).not.toHaveBeenCalled(); + }); + + it('does not call onBreak when offline', async () => { + const expectedError = new TypeError('Failed to fetch'); + const mockFetch = jest.fn(() => { + throw expectedError; + }); + const endpointUrl = 'https://rpc.example.chain'; + const onBreakListener = jest.fn(); + const service = new RpcService({ + fetch: mockFetch, + btoa, + endpointUrl, + isOffline: (): boolean => true, + }); + service.onRetry(() => { + clock.next(); + }); + service.onBreak(onBreakListener); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + // Make multiple requests - even though we'd normally break the circuit, + // when offline, no retries happen so circuit won't break + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + + // When offline, retries don't happen, so circuit won't break and onBreak + // should not be called + expect(onBreakListener).not.toHaveBeenCalled(); + }); + }); + it('removes non-JSON-RPC-compliant properties from the request body before sending it to the endpoint', async () => { const endpointUrl = 'https://rpc.example.chain'; nock(endpointUrl) @@ -612,6 +722,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); // @ts-expect-error Intentionally passing bad input. @@ -657,6 +768,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl: 'https://username:password@rpc.example.chain', + isOffline: (): boolean => false, }); const response = await service.request({ @@ -696,6 +808,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl: 'https://username:password@rpc.example.chain', + isOffline: (): boolean => false, }); await service.request({ @@ -731,6 +844,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl: 'https://username:password@rpc.example.chain', + isOffline: (): boolean => false, }); await service.request( @@ -780,6 +894,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); const response = await service.request({ @@ -826,6 +941,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); const response = await service.request( @@ -865,6 +981,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); const response = await service.request({ @@ -906,6 +1023,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onDegraded(onDegradedListener); @@ -943,6 +1061,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onAvailable(onAvailableListener); @@ -994,6 +1113,7 @@ describe('RpcService', () => { fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onAvailable(onAvailableListener); @@ -1052,7 +1172,12 @@ function testsForNonRetriableErrors({ // do nothing }, createService = (args): RpcService => { - return new RpcService({ fetch, btoa, endpointUrl: args.endpointUrl }); + return new RpcService({ + fetch, + btoa, + endpointUrl: args.endpointUrl, + isOffline: (): boolean => false, + }); }, endpointUrl = 'https://rpc.example.chain', rpcMethod = `eth_chainId`, @@ -1190,6 +1315,7 @@ function testsForRetriableFetchErrors({ fetch: mockFetch, btoa, endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1218,6 +1344,7 @@ function testsForRetriableFetchErrors({ fetch: mockFetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1252,6 +1379,7 @@ function testsForRetriableFetchErrors({ fetch: mockFetch, btoa, endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1282,6 +1410,7 @@ function testsForRetriableFetchErrors({ fetch: mockFetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1318,6 +1447,7 @@ function testsForRetriableFetchErrors({ btoa, endpointUrl, logger, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1358,6 +1488,7 @@ function testsForRetriableFetchErrors({ btoa, endpointUrl, logger, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1405,6 +1536,7 @@ function testsForRetriableFetchErrors({ fetch: mockFetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onAvailable(onAvailableListener); service.onRetry(() => { @@ -1473,6 +1605,7 @@ function testsForRetriableResponses({ fetch, btoa, endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1505,6 +1638,7 @@ function testsForRetriableResponses({ fetch, btoa, endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1541,6 +1675,7 @@ function testsForRetriableResponses({ fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1582,6 +1717,7 @@ function testsForRetriableResponses({ fetch, btoa, endpointUrl, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1630,6 +1766,7 @@ function testsForRetriableResponses({ btoa, endpointUrl, logger, + isOffline: (): boolean => false, }); service.onRetry(() => { clock.next(); @@ -1654,5 +1791,87 @@ function testsForRetriableResponses({ ); }); + it('does not retry when offline, only makes one request', async () => { + const clock = getClock(); + const scope = nock('https://rpc.example.chain') + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }) + .times(1) + .reply(httpStatus, responseBody); + const service = new RpcService({ + fetch, + btoa, + endpointUrl: 'https://rpc.example.chain', + isOffline: (): boolean => true, + }); + service.onRetry(() => { + clock.next(); + }); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + // When offline, no retries should happen, so only 1 request + expect(scope.isDone()).toBe(true); + }); + + it('does not call onBreak when offline', async () => { + const clock = getClock(); + const scope = nock('https://rpc.example.chain') + .post('/', { + id: 1, + jsonrpc: '2.0', + method: 'eth_chainId', + params: [], + }) + .times(3) + .reply(httpStatus, responseBody); + const endpointUrl = 'https://rpc.example.chain'; + const onBreakListener = jest.fn(); + const service = new RpcService({ + fetch, + btoa, + endpointUrl, + isOffline: (): boolean => true, + }); + service.onRetry(() => { + clock.next(); + }); + service.onBreak(onBreakListener); + + const jsonRpcRequest = { + id: 1, + jsonrpc: '2.0' as const, + method: 'eth_chainId', + params: [], + }; + // Make multiple requests - even though we'd normally break the circuit, + // when offline, no retries happen so circuit won't break + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + await expect(service.request(jsonRpcRequest)).rejects.toThrow( + expectedError, + ); + + // When offline, retries don't happen, so circuit won't break and onBreak + // should not be called + expect(onBreakListener).not.toHaveBeenCalled(); + expect(scope.isDone()).toBe(true); + }); + /* eslint-enable jest/require-top-level-describe,jest/no-identical-title */ } diff --git a/packages/network-controller/src/rpc-service/rpc-service.ts b/packages/network-controller/src/rpc-service/rpc-service.ts index df09eda0e0a..32e24494526 100644 --- a/packages/network-controller/src/rpc-service/rpc-service.ts +++ b/packages/network-controller/src/rpc-service/rpc-service.ts @@ -57,6 +57,12 @@ export type RpcServiceOptions = { * not accepted, as it is overwritten. See {@link createServicePolicy}. */ policyOptions?: Omit; + /** + * A function that checks if the user is currently offline. If it returns true, + * connection errors will not be retried, preventing degraded and break + * callbacks from being triggered. + */ + isOffline: () => boolean; }; const log = createModuleLogger(projectLogger, 'RpcService'); @@ -277,6 +283,7 @@ export class RpcService implements AbstractRpcService { logger, fetchOptions = {}, policyOptions = {}, + isOffline, } = options; this.#fetch = givenFetch; @@ -294,6 +301,12 @@ export class RpcService implements AbstractRpcService { maxConsecutiveFailures: DEFAULT_MAX_CONSECUTIVE_FAILURES, ...policyOptions, retryFilterPolicy: handleWhen((error) => { + // If user is offline, don't retry any errors + // This prevents degraded/break callbacks from being triggered + if (isOffline()) { + return false; + } + return ( // Ignore errors where the request failed to establish isConnectionError(error) || diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 1937cfd9b8b..311333834ee 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -179,6 +179,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -208,6 +209,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -244,6 +246,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -279,6 +282,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -314,6 +318,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -359,6 +364,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }), ).toThrow( @@ -399,6 +405,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }); @@ -452,6 +459,7 @@ describe('NetworkController', () => { > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }); @@ -4635,6 +4643,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -6065,6 +6074,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -6297,6 +6307,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -7296,6 +7307,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -8165,6 +8177,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -9163,6 +9176,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -10328,6 +10342,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -11044,6 +11059,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -11778,6 +11794,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', @@ -12487,6 +12504,7 @@ describe('NetworkController', () => { > => ({ btoa, fetch, + isOffline: (): boolean => false, fetchOptions: { headers: { 'X-Foo': 'Bar', diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index 039bc41c759..860eb2a5347 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -1,3 +1,5 @@ +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; +import type { ConnectivityStatus } from '@metamask/connectivity-controller'; import { ChainId, InfuraNetworkType, @@ -84,15 +86,29 @@ export const TESTNET = { * Build a root messenger that includes all events used by the network * controller. * + * @param options - Optional configuration. + * @param options.connectivityStatus - The connectivity status to return by default. + * If not provided, defaults to Online. * @returns The messenger. */ -export function buildRootMessenger(): RootMessenger { +export function buildRootMessenger({ + connectivityStatus = CONNECTIVITY_STATUSES.Online, +}: { + connectivityStatus?: ConnectivityStatus; +} = {}): RootMessenger { const rootMessenger = new Messenger< MockAnyNamespace, MessengerActions, MessengerEvents >({ namespace: MOCK_ANY_NAMESPACE, captureException: jest.fn() }); + rootMessenger.registerActionHandler( + 'ConnectivityController:getState', + () => ({ + connectivityStatus, + }), + ); + return rootMessenger; } @@ -115,6 +131,11 @@ export function buildNetworkControllerMessenger( parent: rootMessenger, }); + rootMessenger.delegate({ + messenger: networkControllerMessenger, + actions: ['ConnectivityController:getState'], + }); + return networkControllerMessenger; } @@ -622,6 +643,7 @@ export async function withController( > => ({ fetch, btoa, + isOffline: (): boolean => false, }), ...rest, }); diff --git a/packages/network-controller/tests/network-client/helpers.ts b/packages/network-controller/tests/network-client/helpers.ts index eb251e1536f..deaf9266e91 100644 --- a/packages/network-controller/tests/network-client/helpers.ts +++ b/packages/network-controller/tests/network-client/helpers.ts @@ -502,7 +502,7 @@ export async function withNetworkClient( getRpcServiceOptions = (): Omit< RpcServiceOptions, 'failoverService' | 'endpointUrl' - > => ({ fetch, btoa }), + > => ({ fetch, btoa, isOffline: (): boolean => false }), getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({}), messenger = buildRootMessenger(), networkClientId = 'some-network-client-id', diff --git a/packages/network-controller/tests/network-client/rpc-failover.ts b/packages/network-controller/tests/network-client/rpc-failover.ts index 0a2029abd43..2decfa2dc72 100644 --- a/packages/network-controller/tests/network-client/rpc-failover.ts +++ b/packages/network-controller/tests/network-client/rpc-failover.ts @@ -88,6 +88,7 @@ export function testsForRpcFailoverBehavior({ getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, @@ -177,7 +178,11 @@ export function testsForRpcFailoverBehavior({ failoverRpcUrls: ['https://failover.endpoint'], messenger, getRpcServiceOptions: (rpcEndpointUrl) => { - const commonOptions = { fetch, btoa }; + const commonOptions = { + fetch, + btoa, + isOffline: (): boolean => false, + }; if (rpcEndpointUrl === 'https://failover.endpoint') { const headers: HeadersInit = { 'X-Baz': 'Qux', @@ -263,6 +268,7 @@ export function testsForRpcFailoverBehavior({ getRpcServiceOptions: () => ({ fetch, btoa, + isOffline: (): boolean => false, policyOptions: { backoff: new ConstantBackoff(backoffDuration), }, diff --git a/packages/network-controller/tsconfig.build.json b/packages/network-controller/tsconfig.build.json index 3aa1aa62e0f..a81948ea66a 100644 --- a/packages/network-controller/tsconfig.build.json +++ b/packages/network-controller/tsconfig.build.json @@ -8,6 +8,7 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../connectivity-controller/tsconfig.build.json" }, { "path": "../eth-block-tracker/tsconfig.build.json" }, { "path": "../eth-json-rpc-middleware/tsconfig.build.json" }, { "path": "../eth-json-rpc-provider/tsconfig.build.json" }, diff --git a/packages/network-controller/tsconfig.json b/packages/network-controller/tsconfig.json index 9f1911d6466..e5a54a5785b 100644 --- a/packages/network-controller/tsconfig.json +++ b/packages/network-controller/tsconfig.json @@ -7,6 +7,7 @@ "references": [ { "path": "../base-controller" }, { "path": "../controller-utils" }, + { "path": "../connectivity-controller" }, { "path": "../eth-block-tracker" }, { "path": "../eth-json-rpc-middleware" }, { "path": "../eth-json-rpc-provider" }, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 9cb52e729d1..a66d5c6dc7f 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -5,6 +5,8 @@ import type { ApprovalControllerEvents, } from '@metamask/approval-controller'; import { ApprovalController } from '@metamask/approval-controller'; +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; +import type { ConnectivityControllerGetStateAction } from '@metamask/connectivity-controller'; import { ApprovalType, BUILT_IN_NETWORKS, @@ -80,6 +82,7 @@ type AllTransactionControllerEvents = type AllActions = | AllTransactionControllerActions | NetworkControllerActions + | ConnectivityControllerGetStateAction | ApprovalControllerActions | AccountsControllerActions | RemoteFeatureFlagControllerGetStateAction; @@ -183,15 +186,26 @@ const setupController = async ( namespace: MOCK_ANY_NAMESPACE, }); + rootMessenger.registerActionHandler( + 'ConnectivityController:getState', + () => ({ + connectivityStatus: CONNECTIVITY_STATUSES.Online, + }), + ); + const networkControllerMessenger = new Messenger< 'NetworkController', - NetworkControllerActions, + NetworkControllerActions | ConnectivityControllerGetStateAction, NetworkControllerEvents, typeof rootMessenger >({ namespace: 'NetworkController', parent: rootMessenger, }); + rootMessenger.delegate({ + messenger: networkControllerMessenger, + actions: ['ConnectivityController:getState'], + }); const networkController = new NetworkController({ messenger: networkControllerMessenger, infuraProjectId, @@ -200,6 +214,7 @@ const setupController = async ( > => ({ fetch, btoa, + isOffline: (): boolean => false, }), }); await networkController.initializeProvider(); diff --git a/yarn.lock b/yarn.lock index fbe3268e03c..3702aaf88ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2964,7 +2964,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/connectivity-controller@workspace:packages/connectivity-controller": +"@metamask/connectivity-controller@npm:^0.0.0, @metamask/connectivity-controller@workspace:packages/connectivity-controller": version: 0.0.0-use.local resolution: "@metamask/connectivity-controller@workspace:packages/connectivity-controller" dependencies: @@ -4207,6 +4207,7 @@ __metadata: "@json-rpc-specification/meta-schema": "npm:^1.0.6" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" + "@metamask/connectivity-controller": "npm:^0.0.0" "@metamask/controller-utils": "npm:^11.18.0" "@metamask/eth-block-tracker": "npm:^15.0.0" "@metamask/eth-json-rpc-infura": "npm:^10.3.0"