@metamask-previews/network-connection-banner-controller 0.1.0-preview-2fc626b → 0.1.0-preview-3c0e8a808

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/README.md +13 -0
  3. package/dist/NetworkConnectionBannerController-method-action-types.cjs.map +1 -1
  4. package/dist/NetworkConnectionBannerController-method-action-types.d.cts +12 -4
  5. package/dist/NetworkConnectionBannerController-method-action-types.d.cts.map +1 -1
  6. package/dist/NetworkConnectionBannerController-method-action-types.d.mts +12 -4
  7. package/dist/NetworkConnectionBannerController-method-action-types.d.mts.map +1 -1
  8. package/dist/NetworkConnectionBannerController-method-action-types.mjs.map +1 -1
  9. package/dist/NetworkConnectionBannerController.cjs +153 -94
  10. package/dist/NetworkConnectionBannerController.cjs.map +1 -1
  11. package/dist/NetworkConnectionBannerController.d.cts +25 -16
  12. package/dist/NetworkConnectionBannerController.d.cts.map +1 -1
  13. package/dist/NetworkConnectionBannerController.d.mts +25 -16
  14. package/dist/NetworkConnectionBannerController.d.mts.map +1 -1
  15. package/dist/NetworkConnectionBannerController.mjs +154 -95
  16. package/dist/NetworkConnectionBannerController.mjs.map +1 -1
  17. package/dist/index.cjs +1 -4
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -3
  20. package/dist/index.d.cts.map +1 -1
  21. package/dist/index.d.mts +2 -3
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +0 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/selectors.cjs.map +1 -1
  26. package/dist/selectors.d.cts +2 -2
  27. package/dist/selectors.d.cts.map +1 -1
  28. package/dist/selectors.d.mts +2 -2
  29. package/dist/selectors.d.mts.map +1 -1
  30. package/dist/selectors.mjs.map +1 -1
  31. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
- - Initial release ([#9041](https://github.com/MetaMask/core/pull/9041))
12
+ - Add `NetworkConnectionBannerController`, which evaluates enabled network RPC
13
+ health after initialization and manages degraded and unavailable banner state,
14
+ dismissal, and switching custom RPC endpoints to an available Infura endpoint
15
+ ([#9041](https://github.com/MetaMask/core/pull/9041))
13
16
 
14
17
  [Unreleased]: https://github.com/MetaMask/core/
package/README.md CHANGED
@@ -5,6 +5,19 @@ connection banner based on RPC endpoint health. It encapsulates the shared
5
5
  rule, the 5s/30s timer state machine, and the eTLD+1 grouping previously
6
6
  duplicated across MetaMask clients.
7
7
 
8
+ ## Initialization
9
+
10
+ After constructing the controller, call `init()` only after the
11
+ `NetworkController`, `NetworkEnablementController`, and
12
+ `ConnectivityController` have initialized. Until then, upstream state changes
13
+ are ignored and no banner timers run.
14
+
15
+ ```typescript
16
+ networkConnectionBannerController.init();
17
+ ```
18
+
19
+ `init()` is idempotent and immediately evaluates the latest upstream state.
20
+
8
21
  ## Installation
9
22
 
10
23
  `yarn add @metamask/network-connection-banner-controller`
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkConnectionBannerController-method-action-types.cjs","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { NetworkConnectionBannerController } from './NetworkConnectionBannerController';\n\n/**\n * Clears the banner state regardless of the current rule outcome. The next\n * subscription-driven evaluation will re-show the banner if the conditions\n * still hold.\n */\nexport type NetworkConnectionBannerControllerDismissBannerAction = {\n type: `NetworkConnectionBannerController:dismissBanner`;\n handler: NetworkConnectionBannerController['dismissBanner'];\n};\n\n/**\n * Switches the chain's default RPC endpoint to its first Infura endpoint,\n * causing the banner to clear once the network becomes available again.\n *\n * @param args - The arguments to this action.\n * @param args.chainId - The chain whose default RPC should be switched.\n * @throws If the chain configuration cannot be found, or if it has no\n * Infura endpoint to switch to, or if the default is already Infura.\n */\nexport type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {\n type: `NetworkConnectionBannerController:switchToDefaultInfuraRpc`;\n handler: NetworkConnectionBannerController['switchToDefaultInfuraRpc'];\n};\n\n/**\n * Union of all NetworkConnectionBannerController action types.\n */\nexport type NetworkConnectionBannerControllerMethodActions =\n | NetworkConnectionBannerControllerDismissBannerAction\n | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;\n"]}
1
+ {"version":3,"file":"NetworkConnectionBannerController-method-action-types.cjs","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { NetworkConnectionBannerController } from './NetworkConnectionBannerController';\n\n/**\n * Starts evaluating network connection state.\n *\n * This method should be called after the upstream network, network\n * enablement, and connectivity controllers have been initialized.\n */\nexport type NetworkConnectionBannerControllerInitAction = {\n type: `NetworkConnectionBannerController:init`;\n handler: NetworkConnectionBannerController['init'];\n};\n\n/**\n * Clears the banner state such that the banner will be hidden.\n */\nexport type NetworkConnectionBannerControllerDismissBannerAction = {\n type: `NetworkConnectionBannerController:dismissBanner`;\n handler: NetworkConnectionBannerController['dismissBanner'];\n};\n\n/**\n * Switches the chain's default RPC endpoint to its first Infura endpoint,\n * causing the banner to clear once the network becomes available again.\n *\n * @param args - The arguments to this action.\n * @param args.chainId - The chain whose default RPC should be switched.\n * @throws If the chain configuration cannot be found, or if it has no\n * Infura endpoint to switch to, or if the default is already Infura.\n */\nexport type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {\n type: `NetworkConnectionBannerController:switchToDefaultInfuraRpc`;\n handler: NetworkConnectionBannerController['switchToDefaultInfuraRpc'];\n};\n\n/**\n * Union of all NetworkConnectionBannerController action types.\n */\nexport type NetworkConnectionBannerControllerMethodActions =\n | NetworkConnectionBannerControllerInitAction\n | NetworkConnectionBannerControllerDismissBannerAction\n | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;\n"]}
@@ -4,9 +4,17 @@
4
4
  */
5
5
  import type { NetworkConnectionBannerController } from "./NetworkConnectionBannerController.cjs";
6
6
  /**
7
- * Clears the banner state regardless of the current rule outcome. The next
8
- * subscription-driven evaluation will re-show the banner if the conditions
9
- * still hold.
7
+ * Starts evaluating network connection state.
8
+ *
9
+ * This method should be called after the upstream network, network
10
+ * enablement, and connectivity controllers have been initialized.
11
+ */
12
+ export type NetworkConnectionBannerControllerInitAction = {
13
+ type: `NetworkConnectionBannerController:init`;
14
+ handler: NetworkConnectionBannerController['init'];
15
+ };
16
+ /**
17
+ * Clears the banner state such that the banner will be hidden.
10
18
  */
11
19
  export type NetworkConnectionBannerControllerDismissBannerAction = {
12
20
  type: `NetworkConnectionBannerController:dismissBanner`;
@@ -28,5 +36,5 @@ export type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {
28
36
  /**
29
37
  * Union of all NetworkConnectionBannerController action types.
30
38
  */
31
- export type NetworkConnectionBannerControllerMethodActions = NetworkConnectionBannerControllerDismissBannerAction | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;
39
+ export type NetworkConnectionBannerControllerMethodActions = NetworkConnectionBannerControllerInitAction | NetworkConnectionBannerControllerDismissBannerAction | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;
32
40
  //# sourceMappingURL=NetworkConnectionBannerController-method-action-types.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkConnectionBannerController-method-action-types.d.cts","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,gDAA4C;AAE7F;;;;GAIG;AACH,MAAM,MAAM,oDAAoD,GAAG;IACjE,IAAI,EAAE,iDAAiD,CAAC;IACxD,OAAO,EAAE,iCAAiC,CAAC,eAAe,CAAC,CAAC;CAC7D,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,+DAA+D,GAAG;IAC5E,IAAI,EAAE,4DAA4D,CAAC;IACnE,OAAO,EAAE,iCAAiC,CAAC,0BAA0B,CAAC,CAAC;CACxE,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,8CAA8C,GACtD,oDAAoD,GACpD,+DAA+D,CAAC"}
1
+ {"version":3,"file":"NetworkConnectionBannerController-method-action-types.d.cts","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,gDAA4C;AAE7F;;;;;GAKG;AACH,MAAM,MAAM,2CAA2C,GAAG;IACxD,IAAI,EAAE,wCAAwC,CAAC;IAC/C,OAAO,EAAE,iCAAiC,CAAC,MAAM,CAAC,CAAC;CACpD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oDAAoD,GAAG;IACjE,IAAI,EAAE,iDAAiD,CAAC;IACxD,OAAO,EAAE,iCAAiC,CAAC,eAAe,CAAC,CAAC;CAC7D,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,+DAA+D,GAAG;IAC5E,IAAI,EAAE,4DAA4D,CAAC;IACnE,OAAO,EAAE,iCAAiC,CAAC,0BAA0B,CAAC,CAAC;CACxE,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,8CAA8C,GACtD,2CAA2C,GAC3C,oDAAoD,GACpD,+DAA+D,CAAC"}
@@ -4,9 +4,17 @@
4
4
  */
5
5
  import type { NetworkConnectionBannerController } from "./NetworkConnectionBannerController.mjs";
6
6
  /**
7
- * Clears the banner state regardless of the current rule outcome. The next
8
- * subscription-driven evaluation will re-show the banner if the conditions
9
- * still hold.
7
+ * Starts evaluating network connection state.
8
+ *
9
+ * This method should be called after the upstream network, network
10
+ * enablement, and connectivity controllers have been initialized.
11
+ */
12
+ export type NetworkConnectionBannerControllerInitAction = {
13
+ type: `NetworkConnectionBannerController:init`;
14
+ handler: NetworkConnectionBannerController['init'];
15
+ };
16
+ /**
17
+ * Clears the banner state such that the banner will be hidden.
10
18
  */
11
19
  export type NetworkConnectionBannerControllerDismissBannerAction = {
12
20
  type: `NetworkConnectionBannerController:dismissBanner`;
@@ -28,5 +36,5 @@ export type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {
28
36
  /**
29
37
  * Union of all NetworkConnectionBannerController action types.
30
38
  */
31
- export type NetworkConnectionBannerControllerMethodActions = NetworkConnectionBannerControllerDismissBannerAction | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;
39
+ export type NetworkConnectionBannerControllerMethodActions = NetworkConnectionBannerControllerInitAction | NetworkConnectionBannerControllerDismissBannerAction | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;
32
40
  //# sourceMappingURL=NetworkConnectionBannerController-method-action-types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkConnectionBannerController-method-action-types.d.mts","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,gDAA4C;AAE7F;;;;GAIG;AACH,MAAM,MAAM,oDAAoD,GAAG;IACjE,IAAI,EAAE,iDAAiD,CAAC;IACxD,OAAO,EAAE,iCAAiC,CAAC,eAAe,CAAC,CAAC;CAC7D,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,+DAA+D,GAAG;IAC5E,IAAI,EAAE,4DAA4D,CAAC;IACnE,OAAO,EAAE,iCAAiC,CAAC,0BAA0B,CAAC,CAAC;CACxE,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,8CAA8C,GACtD,oDAAoD,GACpD,+DAA+D,CAAC"}
1
+ {"version":3,"file":"NetworkConnectionBannerController-method-action-types.d.mts","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,gDAA4C;AAE7F;;;;;GAKG;AACH,MAAM,MAAM,2CAA2C,GAAG;IACxD,IAAI,EAAE,wCAAwC,CAAC;IAC/C,OAAO,EAAE,iCAAiC,CAAC,MAAM,CAAC,CAAC;CACpD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oDAAoD,GAAG;IACjE,IAAI,EAAE,iDAAiD,CAAC;IACxD,OAAO,EAAE,iCAAiC,CAAC,eAAe,CAAC,CAAC;CAC7D,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,+DAA+D,GAAG;IAC5E,IAAI,EAAE,4DAA4D,CAAC;IACnE,OAAO,EAAE,iCAAiC,CAAC,0BAA0B,CAAC,CAAC;CACxE,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,8CAA8C,GACtD,2CAA2C,GAC3C,oDAAoD,GACpD,+DAA+D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkConnectionBannerController-method-action-types.mjs","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { NetworkConnectionBannerController } from './NetworkConnectionBannerController';\n\n/**\n * Clears the banner state regardless of the current rule outcome. The next\n * subscription-driven evaluation will re-show the banner if the conditions\n * still hold.\n */\nexport type NetworkConnectionBannerControllerDismissBannerAction = {\n type: `NetworkConnectionBannerController:dismissBanner`;\n handler: NetworkConnectionBannerController['dismissBanner'];\n};\n\n/**\n * Switches the chain's default RPC endpoint to its first Infura endpoint,\n * causing the banner to clear once the network becomes available again.\n *\n * @param args - The arguments to this action.\n * @param args.chainId - The chain whose default RPC should be switched.\n * @throws If the chain configuration cannot be found, or if it has no\n * Infura endpoint to switch to, or if the default is already Infura.\n */\nexport type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {\n type: `NetworkConnectionBannerController:switchToDefaultInfuraRpc`;\n handler: NetworkConnectionBannerController['switchToDefaultInfuraRpc'];\n};\n\n/**\n * Union of all NetworkConnectionBannerController action types.\n */\nexport type NetworkConnectionBannerControllerMethodActions =\n | NetworkConnectionBannerControllerDismissBannerAction\n | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;\n"]}
1
+ {"version":3,"file":"NetworkConnectionBannerController-method-action-types.mjs","sourceRoot":"","sources":["../src/NetworkConnectionBannerController-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { NetworkConnectionBannerController } from './NetworkConnectionBannerController';\n\n/**\n * Starts evaluating network connection state.\n *\n * This method should be called after the upstream network, network\n * enablement, and connectivity controllers have been initialized.\n */\nexport type NetworkConnectionBannerControllerInitAction = {\n type: `NetworkConnectionBannerController:init`;\n handler: NetworkConnectionBannerController['init'];\n};\n\n/**\n * Clears the banner state such that the banner will be hidden.\n */\nexport type NetworkConnectionBannerControllerDismissBannerAction = {\n type: `NetworkConnectionBannerController:dismissBanner`;\n handler: NetworkConnectionBannerController['dismissBanner'];\n};\n\n/**\n * Switches the chain's default RPC endpoint to its first Infura endpoint,\n * causing the banner to clear once the network becomes available again.\n *\n * @param args - The arguments to this action.\n * @param args.chainId - The chain whose default RPC should be switched.\n * @throws If the chain configuration cannot be found, or if it has no\n * Infura endpoint to switch to, or if the default is already Infura.\n */\nexport type NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction = {\n type: `NetworkConnectionBannerController:switchToDefaultInfuraRpc`;\n handler: NetworkConnectionBannerController['switchToDefaultInfuraRpc'];\n};\n\n/**\n * Union of all NetworkConnectionBannerController action types.\n */\nexport type NetworkConnectionBannerControllerMethodActions =\n | NetworkConnectionBannerControllerInitAction\n | NetworkConnectionBannerControllerDismissBannerAction\n | NetworkConnectionBannerControllerSwitchToDefaultInfuraRpcAction;\n"]}
@@ -10,14 +10,44 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
11
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
12
  };
13
- var _NetworkConnectionBannerController_instances, _NetworkConnectionBannerController_degradedTimer, _NetworkConnectionBannerController_unavailableTimer, _NetworkConnectionBannerController_pendingNetworkClientId, _NetworkConnectionBannerController_evaluate, _NetworkConnectionBannerController_resetBanner, _NetworkConnectionBannerController_clearTimers, _NetworkConnectionBannerController_findFailedNetworkForBanner;
13
+ var _NetworkConnectionBannerController_instances, _NetworkConnectionBannerController_degradedTimer, _NetworkConnectionBannerController_unavailableTimer, _NetworkConnectionBannerController_pendingNetworkClientId, _NetworkConnectionBannerController_initialized, _NetworkConnectionBannerController_onUpstreamChange, _NetworkConnectionBannerController_refreshState, _NetworkConnectionBannerController_resetBanner, _NetworkConnectionBannerController_clearTimers, _NetworkConnectionBannerController_findFailedNetwork, _NetworkConnectionBannerController_getEnabledEvmChainIds, _NetworkConnectionBannerController_collectNetworksWithMetadata, _NetworkConnectionBannerController_buildFailedNetwork, _NetworkConnectionBannerController_pickBannerNetwork;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.NetworkConnectionBannerController = exports.getDefaultNetworkConnectionBannerControllerState = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
17
  const connectivity_controller_1 = require("@metamask/connectivity-controller");
18
18
  const network_controller_1 = require("@metamask/network-controller");
19
+ const network_enablement_controller_1 = require("@metamask/network-enablement-controller");
19
20
  const utils_1 = require("@metamask/utils");
21
+ const reselect_1 = require("reselect");
20
22
  const url_utils_1 = require("./url-utils.cjs");
23
+ /**
24
+ * Selects `networksMetadata` from the `NetworkController` state.
25
+ *
26
+ * @param state - The `NetworkController` state.
27
+ * @returns The networks metadata map keyed by network client id.
28
+ */
29
+ const selectNetworksMetadata = (state) => state.networksMetadata;
30
+ /**
31
+ * Selects `networkConfigurationsByChainId` from the `NetworkController`
32
+ * state.
33
+ *
34
+ * @param state - The `NetworkController` state.
35
+ * @returns The network configurations keyed by chain id.
36
+ */
37
+ const selectNetworkConfigurationsByChainId = (state) => state.networkConfigurationsByChainId;
38
+ /**
39
+ * Selects the `NetworkController` state fields that influence the banner
40
+ * rule. Composed with `createSelector` so the return object stays reference
41
+ * stable while unrelated `NetworkController` state (e.g.
42
+ * `selectedNetworkClientId`) changes.
43
+ *
44
+ * @param state - The `NetworkController` state.
45
+ * @returns The relevant network fields.
46
+ */
47
+ const selectNetworkControllerFields = (0, reselect_1.createSelector)([selectNetworksMetadata, selectNetworkConfigurationsByChainId], (networksMetadata, networkConfigurationsByChainId) => ({
48
+ networksMetadata,
49
+ networkConfigurationsByChainId,
50
+ }));
21
51
  /**
22
52
  * The name of the {@link NetworkConnectionBannerController}, used to namespace
23
53
  * the controller's actions and events and to namespace the controller's state
@@ -50,9 +80,18 @@ function getDefaultNetworkConnectionBannerControllerState() {
50
80
  };
51
81
  }
52
82
  exports.getDefaultNetworkConnectionBannerControllerState = getDefaultNetworkConnectionBannerControllerState;
53
- const DEGRADED_BANNER_TIMEOUT_MS = 5000;
54
- const UNAVAILABLE_BANNER_TIMEOUT_MS = 30000;
83
+ /**
84
+ * How long (in milliseconds) a failing network must remain in a "failed"
85
+ * status ("degraded" or "unavailable") before the degraded banner appears.
86
+ */
87
+ const DEGRADED_BANNER_TIMEOUT = 5000;
88
+ /**
89
+ * How long (in milliseconds) a failing network must remain in a "failed"
90
+ * status before the banner escalates to "unavailable".
91
+ */
92
+ const UNAVAILABLE_BANNER_TIMEOUT = 30000;
55
93
  const MESSENGER_EXPOSED_METHODS = [
94
+ 'init',
56
95
  'dismissBanner',
57
96
  'switchToDefaultInfuraRpc',
58
97
  ];
@@ -67,8 +106,8 @@ const MESSENGER_EXPOSED_METHODS = [
67
106
  * - The first failing network's default RPC is a custom (non-Infura) endpoint
68
107
  * — users always want to be informed about errors with RPCs they've chosen.
69
108
  * - Failed RPCs span more than one registrable domain (likely client-side).
70
- * - Every enabled EVM network is failing (escape hatch for single-network
71
- * setups so they still get a signal).
109
+ * - Every enabled EVM network with known connectivity status is failing
110
+ * (escape hatch for single-network setups so they still get a signal).
72
111
  *
73
112
  * A wide single-provider outage (e.g. every `*.infura.io` network goes down at
74
113
  * once) collapses to one domain and is suppressed, except in the all-down
@@ -97,20 +136,34 @@ class NetworkConnectionBannerController extends base_controller_1.BaseController
97
136
  _NetworkConnectionBannerController_degradedTimer.set(this, void 0);
98
137
  _NetworkConnectionBannerController_unavailableTimer.set(this, void 0);
99
138
  _NetworkConnectionBannerController_pendingNetworkClientId.set(this, void 0);
100
- const onStateChange = () => __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_evaluate).call(this);
139
+ _NetworkConnectionBannerController_initialized.set(this, false);
140
+ // Scoped selectors per controller guideline so unrelated upstream
141
+ // `stateChange` events (e.g. a `NetworkController` selected client id
142
+ // update) do not trigger a re-evaluation.
101
143
  // Upstream controllers still expose :stateChange; switch to :stateChanged
102
144
  // once those packages migrate their event types.
103
145
  /* eslint-disable no-restricted-syntax -- awaiting upstream :stateChanged migration */
104
- this.messenger.subscribe('NetworkController:stateChange', onStateChange);
105
- this.messenger.subscribe('NetworkEnablementController:stateChange', onStateChange);
106
- this.messenger.subscribe('ConnectivityController:stateChange', onStateChange);
146
+ this.messenger.subscribe('NetworkController:stateChange', () => __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_onUpstreamChange).call(this), selectNetworkControllerFields);
147
+ this.messenger.subscribe('NetworkEnablementController:stateChange', () => __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_onUpstreamChange).call(this), network_enablement_controller_1.selectEnabledNetworkMap);
148
+ this.messenger.subscribe('ConnectivityController:stateChange', () => __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_onUpstreamChange).call(this), connectivity_controller_1.connectivityControllerSelectors.selectConnectivityStatus);
107
149
  /* eslint-enable no-restricted-syntax */
108
150
  this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
109
151
  }
110
152
  /**
111
- * Clears the banner state regardless of the current rule outcome. The next
112
- * subscription-driven evaluation will re-show the banner if the conditions
113
- * still hold.
153
+ * Starts evaluating network connection state.
154
+ *
155
+ * This method should be called after the upstream network, network
156
+ * enablement, and connectivity controllers have been initialized.
157
+ */
158
+ init() {
159
+ if (__classPrivateFieldGet(this, _NetworkConnectionBannerController_initialized, "f")) {
160
+ return;
161
+ }
162
+ __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_refreshState).call(this);
163
+ __classPrivateFieldSet(this, _NetworkConnectionBannerController_initialized, true, "f");
164
+ }
165
+ /**
166
+ * Clears the banner state such that the banner will be hidden.
114
167
  */
115
168
  dismissBanner() {
116
169
  __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_resetBanner).call(this);
@@ -129,7 +182,7 @@ class NetworkConnectionBannerController extends base_controller_1.BaseController
129
182
  if (!networkConfiguration) {
130
183
  throw new Error(`No network configuration found for chain ID "${chainId}".`);
131
184
  }
132
- const infuraEndpointIndex = networkConfiguration.rpcEndpoints.findIndex((endpoint) => isInfuraEndpoint(endpoint.url));
185
+ const infuraEndpointIndex = networkConfiguration.rpcEndpoints.findIndex((endpoint) => getIsInfuraEndpoint(endpoint.url));
133
186
  if (infuraEndpointIndex === -1) {
134
187
  throw new Error(`No Infura endpoint available for chain ID "${chainId}".`);
135
188
  }
@@ -144,66 +197,66 @@ class NetworkConnectionBannerController extends base_controller_1.BaseController
144
197
  }
145
198
  }
146
199
  exports.NetworkConnectionBannerController = NetworkConnectionBannerController;
147
- _NetworkConnectionBannerController_degradedTimer = new WeakMap(), _NetworkConnectionBannerController_unavailableTimer = new WeakMap(), _NetworkConnectionBannerController_pendingNetworkClientId = new WeakMap(), _NetworkConnectionBannerController_instances = new WeakSet(), _NetworkConnectionBannerController_evaluate = function _NetworkConnectionBannerController_evaluate() {
200
+ _NetworkConnectionBannerController_degradedTimer = new WeakMap(), _NetworkConnectionBannerController_unavailableTimer = new WeakMap(), _NetworkConnectionBannerController_pendingNetworkClientId = new WeakMap(), _NetworkConnectionBannerController_initialized = new WeakMap(), _NetworkConnectionBannerController_instances = new WeakSet(), _NetworkConnectionBannerController_onUpstreamChange = function _NetworkConnectionBannerController_onUpstreamChange() {
201
+ if (__classPrivateFieldGet(this, _NetworkConnectionBannerController_initialized, "f")) {
202
+ __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_refreshState).call(this);
203
+ }
204
+ }, _NetworkConnectionBannerController_refreshState = function _NetworkConnectionBannerController_refreshState() {
148
205
  const { connectivityStatus } = this.messenger.call('ConnectivityController:getState');
149
206
  if (connectivityStatus === connectivity_controller_1.CONNECTIVITY_STATUSES.Offline) {
150
207
  __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_resetBanner).call(this);
151
208
  return;
152
209
  }
153
- const failed = __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_findFailedNetworkForBanner).call(this);
154
- if (!failed) {
210
+ const failedNetwork = __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_findFailedNetwork).call(this);
211
+ if (!failedNetwork) {
155
212
  __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_resetBanner).call(this);
156
213
  return;
157
214
  }
158
215
  if (this.state.status !== 'available' &&
159
- this.state.network?.networkClientId === failed.networkClientId) {
160
- this.update((draft) => {
161
- draft.network = failed;
216
+ this.state.network?.networkClientId === failedNetwork.networkClientId) {
217
+ this.update((state) => {
218
+ state.network = failedNetwork;
162
219
  });
163
220
  return;
164
221
  }
165
- if (__classPrivateFieldGet(this, _NetworkConnectionBannerController_pendingNetworkClientId, "f") === failed.networkClientId) {
222
+ if (__classPrivateFieldGet(this, _NetworkConnectionBannerController_pendingNetworkClientId, "f") === failedNetwork.networkClientId) {
166
223
  return;
167
224
  }
168
225
  __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_clearTimers).call(this);
169
- this.update((draft) => {
170
- draft.status = 'available';
171
- draft.network = null;
226
+ this.update((state) => {
227
+ state.status = 'available';
228
+ state.network = null;
172
229
  });
173
- __classPrivateFieldSet(this, _NetworkConnectionBannerController_pendingNetworkClientId, failed.networkClientId, "f");
230
+ // Capture the failing network at schedule time. We trust the messenger
231
+ // contract: if the failure resolves or the target changes during the
232
+ // wait, our upstream subscriptions will have cancelled or replaced this
233
+ // timer via `#clearTimers` before it fires.
234
+ __classPrivateFieldSet(this, _NetworkConnectionBannerController_pendingNetworkClientId, failedNetwork.networkClientId, "f");
174
235
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_degradedTimer, setTimeout(() => {
175
236
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_degradedTimer, undefined, "f");
176
237
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_pendingNetworkClientId, undefined, "f");
177
- const stillFailed = __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_findFailedNetworkForBanner).call(this);
178
- if (!stillFailed) {
179
- return;
180
- }
181
- this.update((draft) => {
182
- draft.status = 'degraded';
183
- draft.network = stillFailed;
238
+ this.update((state) => {
239
+ state.status = 'degraded';
240
+ state.network = failedNetwork;
184
241
  });
185
242
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_unavailableTimer, setTimeout(() => {
186
243
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_unavailableTimer, undefined, "f");
187
- const stillFailedAtEscalation = __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_findFailedNetworkForBanner).call(this);
188
- if (!stillFailedAtEscalation) {
189
- return;
190
- }
191
- this.update((draft) => {
192
- draft.status = 'unavailable';
193
- draft.network = stillFailedAtEscalation;
244
+ this.update((state) => {
245
+ state.status = 'unavailable';
246
+ state.network = failedNetwork;
194
247
  });
195
- }, UNAVAILABLE_BANNER_TIMEOUT_MS - DEGRADED_BANNER_TIMEOUT_MS), "f");
196
- }, DEGRADED_BANNER_TIMEOUT_MS), "f");
248
+ }, UNAVAILABLE_BANNER_TIMEOUT - DEGRADED_BANNER_TIMEOUT), "f");
249
+ }, DEGRADED_BANNER_TIMEOUT), "f");
197
250
  }, _NetworkConnectionBannerController_resetBanner = function _NetworkConnectionBannerController_resetBanner() {
198
251
  __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_clearTimers).call(this);
252
+ __classPrivateFieldSet(this, _NetworkConnectionBannerController_pendingNetworkClientId, undefined, "f");
199
253
  if (this.state.status !== 'available' || this.state.network !== null) {
200
- this.update((draft) => {
201
- draft.status = 'available';
202
- draft.network = null;
254
+ this.update((state) => {
255
+ state.status = 'available';
256
+ state.network = null;
203
257
  });
204
258
  }
205
259
  }, _NetworkConnectionBannerController_clearTimers = function _NetworkConnectionBannerController_clearTimers() {
206
- __classPrivateFieldSet(this, _NetworkConnectionBannerController_pendingNetworkClientId, undefined, "f");
207
260
  if (__classPrivateFieldGet(this, _NetworkConnectionBannerController_degradedTimer, "f") !== undefined) {
208
261
  clearTimeout(__classPrivateFieldGet(this, _NetworkConnectionBannerController_degradedTimer, "f"));
209
262
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_degradedTimer, undefined, "f");
@@ -212,48 +265,64 @@ _NetworkConnectionBannerController_degradedTimer = new WeakMap(), _NetworkConnec
212
265
  clearTimeout(__classPrivateFieldGet(this, _NetworkConnectionBannerController_unavailableTimer, "f"));
213
266
  __classPrivateFieldSet(this, _NetworkConnectionBannerController_unavailableTimer, undefined, "f");
214
267
  }
215
- }, _NetworkConnectionBannerController_findFailedNetworkForBanner = function _NetworkConnectionBannerController_findFailedNetworkForBanner() {
268
+ }, _NetworkConnectionBannerController_findFailedNetwork = function _NetworkConnectionBannerController_findFailedNetwork() {
269
+ const networksWithMetadata = __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_collectNetworksWithMetadata).call(this);
270
+ const failedNetworks = networksWithMetadata
271
+ .filter(({ metadata }) => metadata.status !== network_controller_1.NetworkStatus.Available)
272
+ .map((network) => __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_buildFailedNetwork).call(this, network));
273
+ return __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_pickBannerNetwork).call(this, failedNetworks, networksWithMetadata.length);
274
+ }, _NetworkConnectionBannerController_getEnabledEvmChainIds = function _NetworkConnectionBannerController_getEnabledEvmChainIds() {
216
275
  const { enabledNetworkMap } = this.messenger.call('NetworkEnablementController:getState');
217
- const enabledEvmChainIds = Object.entries(enabledNetworkMap[utils_1.KnownCaipNamespace.Eip155] ?? {})
276
+ return Object.entries(enabledNetworkMap[utils_1.KnownCaipNamespace.Eip155] ?? {})
218
277
  .filter(([, enabled]) => enabled)
219
278
  .map(([chainId]) => chainId);
279
+ }, _NetworkConnectionBannerController_collectNetworksWithMetadata = function _NetworkConnectionBannerController_collectNetworksWithMetadata() {
220
280
  const { networkConfigurationsByChainId, networksMetadata } = this.messenger.call('NetworkController:getState');
221
- const failedNetworks = [];
222
- let totalEnabled = 0;
223
- for (const chainId of enabledEvmChainIds) {
281
+ return __classPrivateFieldGet(this, _NetworkConnectionBannerController_instances, "m", _NetworkConnectionBannerController_getEnabledEvmChainIds).call(this).flatMap((chainId) => {
224
282
  const networkConfiguration = networkConfigurationsByChainId[chainId];
225
283
  if (!networkConfiguration) {
226
- continue;
284
+ return [];
227
285
  }
228
286
  const { rpcEndpoints, defaultRpcEndpointIndex, name } = networkConfiguration;
229
287
  const defaultRpcEndpoint = rpcEndpoints[defaultRpcEndpointIndex];
230
288
  if (!defaultRpcEndpoint) {
231
- continue;
289
+ return [];
232
290
  }
233
- totalEnabled += 1;
234
291
  const metadata = networksMetadata[defaultRpcEndpoint.networkClientId];
235
- if (metadata === undefined ||
236
- metadata.status === network_controller_1.NetworkStatus.Available) {
237
- continue;
238
- }
239
- const endpointIsInfura = isInfuraEndpoint(defaultRpcEndpoint.url);
240
- // For custom endpoints (non-Infura), find an Infura endpoint on this
241
- // chain that we could offer to switch to.
242
- let infuraNetworkClientId = null;
243
- if (!endpointIsInfura) {
244
- const otherInfura = rpcEndpoints.find((endpoint, index) => index !== defaultRpcEndpointIndex && isInfuraEndpoint(endpoint.url));
245
- infuraNetworkClientId = otherInfura?.networkClientId ?? null;
292
+ if (!metadata) {
293
+ return [];
246
294
  }
247
- failedNetworks.push({
248
- chainId,
249
- networkClientId: defaultRpcEndpoint.networkClientId,
250
- networkName: name,
251
- rpcUrl: defaultRpcEndpoint.url,
252
- isInfuraEndpoint: endpointIsInfura,
253
- infuraNetworkClientId,
254
- domain: (0, url_utils_1.getDomain)(defaultRpcEndpoint.url),
255
- });
295
+ return [
296
+ {
297
+ chainId,
298
+ name,
299
+ rpcEndpoints,
300
+ defaultRpcEndpointIndex,
301
+ defaultRpcEndpoint,
302
+ metadata,
303
+ },
304
+ ];
305
+ });
306
+ }, _NetworkConnectionBannerController_buildFailedNetwork = function _NetworkConnectionBannerController_buildFailedNetwork({ chainId, name, rpcEndpoints, defaultRpcEndpointIndex, defaultRpcEndpoint, }) {
307
+ const isInfuraEndpoint = getIsInfuraEndpoint(defaultRpcEndpoint.url);
308
+ // For custom endpoints (non-Infura), find an Infura endpoint on this
309
+ // chain that we could offer to switch to.
310
+ let switchableInfuraNetworkClientId = null;
311
+ if (!isInfuraEndpoint) {
312
+ const otherInfura = rpcEndpoints.find((endpoint, index) => index !== defaultRpcEndpointIndex &&
313
+ getIsInfuraEndpoint(endpoint.url));
314
+ switchableInfuraNetworkClientId = otherInfura?.networkClientId ?? null;
256
315
  }
316
+ return {
317
+ chainId,
318
+ networkClientId: defaultRpcEndpoint.networkClientId,
319
+ name,
320
+ rpcUrl: defaultRpcEndpoint.url,
321
+ isInfuraEndpoint,
322
+ switchableInfuraNetworkClientId,
323
+ domain: (0, url_utils_1.getDomain)(defaultRpcEndpoint.url),
324
+ };
325
+ }, _NetworkConnectionBannerController_pickBannerNetwork = function _NetworkConnectionBannerController_pickBannerNetwork(failedNetworks, totalNetworksWithMetadata) {
257
326
  if (failedNetworks.length === 0) {
258
327
  return null;
259
328
  }
@@ -261,36 +330,26 @@ _NetworkConnectionBannerController_degradedTimer = new WeakMap(), _NetworkConnec
261
330
  const distinctDomains = new Set(failedNetworks
262
331
  .map((entry) => entry.domain)
263
332
  .filter((domain) => domain !== null)).size;
264
- const areAllEnabledNetworksFailed = failedNetworks.length === totalEnabled;
333
+ const areAllKnownNetworksFailed = failedNetworks.length === totalNetworksWithMetadata;
265
334
  if (!firstCustomFailed &&
266
335
  distinctDomains <= 1 &&
267
- !areAllEnabledNetworksFailed) {
336
+ !areAllKnownNetworksFailed) {
268
337
  return null;
269
338
  }
270
- const selected = firstCustomFailed ?? failedNetworks[0];
271
- return {
272
- chainId: selected.chainId,
273
- networkClientId: selected.networkClientId,
274
- networkName: selected.networkName,
275
- rpcUrl: selected.rpcUrl,
276
- isInfuraEndpoint: selected.isInfuraEndpoint,
277
- infuraNetworkClientId: selected.infuraNetworkClientId,
278
- };
339
+ return firstCustomFailed ?? failedNetworks[0];
279
340
  };
280
341
  /**
281
- * Checks if an RPC URL is hosted on the Infura service. Detection is by
282
- * hostname suffix rather than the exact MetaMask-key URL pattern, so any
283
- * `*.infura.io` URL counts. That's the right shape for grouping by provider.
342
+ * Whether an RPC URL is a MetaMask Infura endpoint. Matches the
343
+ * `{infuraProjectId}` placeholder form that lives on stored network
344
+ * configurations before the wallet substitutes the real project id at
345
+ * request time. URLs that already carry a substituted key are treated as
346
+ * custom (we do not have the project id in this package).
284
347
  *
285
348
  * @param url - The RPC URL to check.
286
- * @returns True if the URL's host is on `infura.io`.
349
+ * @returns True if the URL is the placeholder form of a MetaMask Infura
350
+ * endpoint.
287
351
  */
288
- function isInfuraEndpoint(url) {
289
- try {
290
- return new URL(url).hostname.toLowerCase().endsWith('.infura.io');
291
- }
292
- catch {
293
- return false;
294
- }
352
+ function getIsInfuraEndpoint(url) {
353
+ return /^https:\/\/[^./]+\.infura\.io\/v3\/\{infuraProjectId\}$/u.test(url);
295
354
  }
296
355
  //# sourceMappingURL=NetworkConnectionBannerController.cjs.map