From 1df131f04649a419867d9604a4f97de19cb43606 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 6 Dec 2024 18:01:28 +0530 Subject: [PATCH 01/23] Release/265.0.0 (#5034) ## Explanation Fixes in signature controller for signature decoding: 1. https://github.com/MetaMask/core/pull/5028 2. https://github.com/MetaMask/core/pull/5033 ## References * Related to: https://github.com/MetaMask/MetaMask-planning/issues/3756 * Related to: https://github.com/MetaMask/MetaMask-planning/issues/3757 ## Changelog NA ## Checklist - [X] I've updated the test suite for new or updated code as appropriate - [X] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [X] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [X] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- package.json | 2 +- packages/signature-controller/CHANGELOG.md | 12 +++++++++++- packages/signature-controller/package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f52ef8ca27..5011c49a20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "264.0.0", + "version": "265.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 8087d9780a..e2e838232a 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [23.1.0] + +### Changed + +- fix: Fixes in signature decoding functionality ([#5028](https://github.com/MetaMask/core/pull/5028)) +- fix: signature decoding api should be called for typed sign V3 also ([#5033](https://github.com/MetaMask/core/pull/5033)) +- fix: Revert `eth-sig-util` package ([#5027](https://github.com/MetaMask/core/pull/5027)) +- fix: Update `jsonschema` version & `eth-sig-util` ([#4998](https://github.com/MetaMask/core/pull/4998)) + ## [23.0.1] ### Changed @@ -431,7 +440,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#1214](https://github.com/MetaMask/core/pull/1214)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@23.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@23.1.0...HEAD +[23.1.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@23.0.1...@metamask/signature-controller@23.1.0 [23.0.1]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@23.0.0...@metamask/signature-controller@23.0.1 [23.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@22.0.0...@metamask/signature-controller@23.0.0 [22.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@21.1.0...@metamask/signature-controller@22.0.0 diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index 1461ca2e92..f852c9d110 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/signature-controller", - "version": "23.0.1", + "version": "23.1.0", "description": "Processes signing requests in order to sign arbitrary and typed data", "keywords": [ "MetaMask", From 0d06e28adfd9d3b3b6cc9e84911c3112b603ce28 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:15:20 +0100 Subject: [PATCH 02/23] chore(deps): bump `swappable-obj-proxy` from `^2.2.0` to `^2.3.0` (#5036) ## Explanation This PR bumps `@metamask/swappable-obj-proxy` by a minor version (to `^2.3.0`) across all packages. ## References ## Changelog ### `@metamask/network-controller` - **CHANGED**: Bump `@metamask/swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ### `@metamask/queued-request-controller` - **CHANGED**: Bump `@metamask/swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ### `@metamask/selected-network-controller` - **CHANGED**: Bump `@metamask/swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/network-controller/package.json | 2 +- packages/queued-request-controller/package.json | 2 +- packages/selected-network-controller/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 75bde730aa..13f7b50a04 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -56,7 +56,7 @@ "@metamask/eth-query": "^4.0.0", "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", - "@metamask/swappable-obj-proxy": "^2.2.0", + "@metamask/swappable-obj-proxy": "^2.3.0", "@metamask/utils": "^10.0.0", "async-mutex": "^0.5.0", "fast-deep-equal": "^3.1.3", diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index dabcc688cc..e9aeba97e3 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -51,7 +51,7 @@ "@metamask/controller-utils": "^11.4.4", "@metamask/json-rpc-engine": "^10.0.1", "@metamask/rpc-errors": "^7.0.1", - "@metamask/swappable-obj-proxy": "^2.2.0", + "@metamask/swappable-obj-proxy": "^2.3.0", "@metamask/utils": "^10.0.0" }, "devDependencies": { diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index ce88371bc8..738bc1b120 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -49,7 +49,7 @@ "dependencies": { "@metamask/base-controller": "^7.0.2", "@metamask/json-rpc-engine": "^10.0.1", - "@metamask/swappable-obj-proxy": "^2.2.0", + "@metamask/swappable-obj-proxy": "^2.3.0", "@metamask/utils": "^10.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6b31313f02..f85ae05bc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3120,7 +3120,7 @@ __metadata: "@metamask/eth-query": "npm:^4.0.0" "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/swappable-obj-proxy": "npm:^2.2.0" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/jest-when": "npm:^2.7.3" @@ -3423,7 +3423,7 @@ __metadata: "@metamask/network-controller": "npm:^22.1.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/selected-network-controller": "npm:^20.0.1" - "@metamask/swappable-obj-proxy": "npm:^2.2.0" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3527,7 +3527,7 @@ __metadata: "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/network-controller": "npm:^22.1.0" "@metamask/permission-controller": "npm:^11.0.4" - "@metamask/swappable-obj-proxy": "npm:^2.2.0" + "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3701,10 +3701,10 @@ __metadata: languageName: node linkType: hard -"@metamask/swappable-obj-proxy@npm:^2.2.0": - version: 2.2.0 - resolution: "@metamask/swappable-obj-proxy@npm:2.2.0" - checksum: 10/bc7a1f496d06327f1db84fe2ed75637b6f2f5db0806d3927f250d5abab9cc70a26ff37283ea7f2db7987e48d2540f6821091d1f3000d6771f29c4d91c402f724 +"@metamask/swappable-obj-proxy@npm:^2.3.0": + version: 2.3.0 + resolution: "@metamask/swappable-obj-proxy@npm:2.3.0" + checksum: 10/1255c599de9237f06df2390719d6dfcb1f168873df61bbaad5ce376efbc057e2030260b94855569313faeb412b7df9b062d209f4b0b163a3dc02f29d42139e1f languageName: node linkType: hard From a4b99231ff2ed406862938d38790611dcc8af37b Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:35:35 +0100 Subject: [PATCH 03/23] chore(deps): bump `@metamask/eth-json-rpc-middleware` to `^15.0.1` (#5037) ## Explanation Bumping `@metamask/eth-json-rpc-middleware` from `^15.0.0` to `^15.0.1` ``` ### Changed - Bump `@metamask/eth-block-tracker` from `^11.0.1` to `^11.0.3` ([#347](https://github.com/MetaMask/eth-json-rpc-middleware/pull/347)) ``` ## References * Related to https://github.com/MetaMask/metamask-extension/issues/17040 ## Changelog ### `@metamask/network-controller` - **CHANGED**: Bump `@metamask/eth-json-rpc-middleware` from `^15.0.0` to `^15.0.1` ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/network-controller/package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 13f7b50a04..00adc26fb7 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -51,7 +51,7 @@ "@metamask/controller-utils": "^11.4.4", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/eth-json-rpc-infura": "^10.0.0", - "@metamask/eth-json-rpc-middleware": "^15.0.0", + "@metamask/eth-json-rpc-middleware": "^15.0.1", "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/eth-query": "^4.0.0", "@metamask/json-rpc-engine": "^10.0.1", diff --git a/yarn.lock b/yarn.lock index f85ae05bc7..10a21f6f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2526,7 +2526,7 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-block-tracker@npm:^11.0.1, @metamask/eth-block-tracker@npm:^11.0.3": +"@metamask/eth-block-tracker@npm:^11.0.3": version: 11.0.3 resolution: "@metamask/eth-block-tracker@npm:11.0.3" dependencies: @@ -2577,11 +2577,11 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-middleware@npm:^15.0.0": - version: 15.0.0 - resolution: "@metamask/eth-json-rpc-middleware@npm:15.0.0" +"@metamask/eth-json-rpc-middleware@npm:^15.0.1": + version: 15.0.1 + resolution: "@metamask/eth-json-rpc-middleware@npm:15.0.1" dependencies: - "@metamask/eth-block-tracker": "npm:^11.0.1" + "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-json-rpc-provider": "npm:^4.1.5" "@metamask/eth-sig-util": "npm:^7.0.3" "@metamask/json-rpc-engine": "npm:^10.0.0" @@ -2592,7 +2592,7 @@ __metadata: klona: "npm:^2.0.6" pify: "npm:^5.0.0" safe-stable-stringify: "npm:^2.4.3" - checksum: 10/3c48d34264c695535f2b4e819fb602d835b6ed37309116a06d04d1b706a7335e0205cd4ccdbf1d3e9dc15ebf40d88954a9a2dc18a91f223dcd6d6392e026a5e9 + checksum: 10/9777fca31440bf0076f5d2c24e2ddb4848ecd9d41b0a5d6114c27339567e60bfcb9057d6bfa81f18f5ca0ffa848ecf9603c765f606b8de206d3e34dba519c501 languageName: node linkType: hard @@ -3115,7 +3115,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-json-rpc-infura": "npm:^10.0.0" - "@metamask/eth-json-rpc-middleware": "npm:^15.0.0" + "@metamask/eth-json-rpc-middleware": "npm:^15.0.1" "@metamask/eth-json-rpc-provider": "npm:^4.1.6" "@metamask/eth-query": "npm:^4.0.0" "@metamask/json-rpc-engine": "npm:^10.0.1" From 2fa588d9a228d58e3244cac6033e24da2bd71183 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:43:43 +0100 Subject: [PATCH 04/23] Release 266.0.0 (#5038) Patch release of `@metamask/network-controller` to update: - `@metamask/eth-block-tracker` - `@metamask/eth-json-rpc-middleware` - `@metamask/swappable-obj-proxy` --- package.json | 2 +- packages/assets-controllers/package.json | 2 +- packages/ens-controller/package.json | 2 +- packages/gas-fee-controller/package.json | 2 +- packages/multichain/package.json | 2 +- packages/network-controller/CHANGELOG.md | 11 ++++++++- packages/network-controller/package.json | 2 +- packages/polling-controller/package.json | 2 +- packages/profile-sync-controller/package.json | 2 +- .../queued-request-controller/package.json | 2 +- .../selected-network-controller/package.json | 2 +- packages/signature-controller/package.json | 2 +- packages/transaction-controller/package.json | 2 +- .../user-operation-controller/package.json | 2 +- yarn.lock | 24 +++++++++---------- 15 files changed, 35 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 5011c49a20..5069a36d68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "265.0.0", + "version": "266.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 47b1172f8d..cffcd780ad 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -81,7 +81,7 @@ "@metamask/ethjs-provider-http": "^0.3.0", "@metamask/keyring-api": "^10.1.0", "@metamask/keyring-controller": "^19.0.1", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/preferences-controller": "^15.0.1", "@metamask/providers": "^18.1.1", "@types/jest": "^27.4.1", diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json index 4caffb1df8..38b282c8f1 100644 --- a/packages/ens-controller/package.json +++ b/packages/ens-controller/package.json @@ -55,7 +55,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json index 9de01a2c8d..06bb62777f 100644 --- a/packages/gas-fee-controller/package.json +++ b/packages/gas-fee-controller/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@babel/runtime": "^7.23.9", "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "@types/jest-when": "^2.7.3", "deepmerge": "^4.2.2", diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 27bfe91f2e..3c39128495 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/permission-controller": "^11.0.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index d51a1c169d..0307f62b01 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] +## [22.1.1] + +### Changed + +- Bump `@metamask/eth-json-rpc-middleware` from `^15.0.0` to `^15.0.1` ([#5037](https://github.com/MetaMask/core/pull/5037)) +- Bump `swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ([#5036](https://github.com/MetaMask/core/pull/5036)) +- Bump `@metamask/eth-block-tracker` from `^11.0.2` to `^11.0.3` ([#5025](https://github.com/MetaMask/core/pull/5025)) + ## [22.1.0] ### Added @@ -686,7 +694,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.1.1...HEAD +[22.1.1]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.1.0...@metamask/network-controller@22.1.1 [22.1.0]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.2...@metamask/network-controller@22.1.0 [22.0.2]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.1...@metamask/network-controller@22.0.2 [22.0.1]: https://github.com/MetaMask/core/compare/@metamask/network-controller@22.0.0...@metamask/network-controller@22.0.1 diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 00adc26fb7..7d4838306c 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/network-controller", - "version": "22.1.0", + "version": "22.1.1", "description": "Provides an interface to the currently selected network via a MetaMask-compatible provider object", "keywords": [ "MetaMask", diff --git a/packages/polling-controller/package.json b/packages/polling-controller/package.json index 98ee02b566..373a912982 100644 --- a/packages/polling-controller/package.json +++ b/packages/polling-controller/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index ab9a811da3..00560e1c3c 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -103,7 +103,7 @@ "@metamask/base-controller": "^7.0.2", "@metamask/keyring-api": "^10.1.0", "@metamask/keyring-controller": "^19.0.1", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/snaps-sdk": "^6.7.0", "@metamask/snaps-utils": "^8.3.0", "@noble/ciphers": "^0.5.2", diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index e9aeba97e3..96ec49ce15 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/selected-network-controller": "^20.0.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index 738bc1b120..5701412dea 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -54,7 +54,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/permission-controller": "^11.0.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index f852c9d110..6a312ede66 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -60,7 +60,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.1", "@metamask/logging-controller": "^6.0.3", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 979d3ff901..4b0d475851 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -76,7 +76,7 @@ "@metamask/eth-json-rpc-provider": "^4.1.6", "@metamask/ethjs-provider-http": "^0.3.0", "@metamask/gas-fee-controller": "^22.0.2", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@types/bn.js": "^5.1.5", "@types/jest": "^27.4.1", "@types/node": "^16.18.54", diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index 7784fe5527..3d790d952f 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -66,7 +66,7 @@ "@metamask/eth-block-tracker": "^11.0.3", "@metamask/gas-fee-controller": "^22.0.2", "@metamask/keyring-controller": "^19.0.1", - "@metamask/network-controller": "^22.1.0", + "@metamask/network-controller": "^22.1.1", "@metamask/transaction-controller": "^42.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/yarn.lock b/yarn.lock index 10a21f6f7d..0f6a3d6acc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2164,7 +2164,7 @@ __metadata: "@metamask/keyring-api": "npm:^10.1.0" "@metamask/keyring-controller": "npm:^19.0.1" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/preferences-controller": "npm:^15.0.1" "@metamask/providers": "npm:^18.1.1" @@ -2461,7 +2461,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -2863,7 +2863,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -3068,7 +3068,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/permission-controller": "npm:^11.0.4" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3105,7 +3105,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.1.0, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.1.1, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: @@ -3300,7 +3300,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/uuid": "npm:^8.3.0" @@ -3360,7 +3360,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/keyring-api": "npm:^10.1.0" "@metamask/keyring-controller": "npm:^19.0.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -3420,7 +3420,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/json-rpc-engine": "npm:^10.0.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/selected-network-controller": "npm:^20.0.1" "@metamask/swappable-obj-proxy": "npm:^2.3.0" @@ -3525,7 +3525,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/json-rpc-engine": "npm:^10.0.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/permission-controller": "npm:^11.0.4" "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^10.0.0" @@ -3557,7 +3557,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/keyring-controller": "npm:^19.0.1" "@metamask/logging-controller": "npm:^6.0.3" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3730,7 +3730,7 @@ __metadata: "@metamask/ethjs-provider-http": "npm:^0.3.0" "@metamask/gas-fee-controller": "npm:^22.0.2" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3774,7 +3774,7 @@ __metadata: "@metamask/eth-query": "npm:^4.0.0" "@metamask/gas-fee-controller": "npm:^22.0.2" "@metamask/keyring-controller": "npm:^19.0.1" - "@metamask/network-controller": "npm:^22.1.0" + "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" From 6899b1e22119f68756d3b0169c7e04ee8619d9c7 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 6 Dec 2024 15:19:34 -0800 Subject: [PATCH 05/23] chore: bump `@metamask/eth-json-rpc-filters` to `^9.0.0` in the `multichain` package (#5040) ## Explanation Bumps `@metamask/eth-json-rpc-filters` to `^9.0.0` in the `multichain` package ## References ## Changelog ### `@metamask/multichain` - **CHANGED**: Bump `@metamask/eth-json-rpc-filters` to `^9.0.0` ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/multichain/package.json | 2 +- yarn.lock | 35 +++++++------------------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 3c39128495..c7014cbe9b 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -49,7 +49,7 @@ "dependencies": { "@metamask/api-specs": "^0.10.12", "@metamask/controller-utils": "^11.4.4", - "@metamask/eth-json-rpc-filters": "^7.0.0", + "@metamask/eth-json-rpc-filters": "^9.0.0", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", "lodash": "^4.17.21" diff --git a/yarn.lock b/yarn.lock index 0f6a3d6acc..625b48a106 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2552,16 +2552,16 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-json-rpc-filters@npm:^7.0.0": - version: 7.0.1 - resolution: "@metamask/eth-json-rpc-filters@npm:7.0.1" +"@metamask/eth-json-rpc-filters@npm:^9.0.0": + version: 9.0.0 + resolution: "@metamask/eth-json-rpc-filters@npm:9.0.0" dependencies: "@metamask/eth-query": "npm:^4.0.0" - "@metamask/json-rpc-engine": "npm:^8.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.0" "@metamask/safe-event-emitter": "npm:^3.0.0" async-mutex: "npm:^0.5.0" pify: "npm:^5.0.0" - checksum: 10/5200f75cee48dfd79deba5e4f1b16ff6827e606da617891f5cb7b59c43ae4ac8420cb9a6a9ca31705c47d2c3d32a3754e052b30f61fd293cc37f009c4fe20c12 + checksum: 10/12095db69902e267d568d67b4241677502558159691d47216f82701f8e2875a051636b8ff483351d17e01d090b7a1eb8d5dec4cc17a9e47c99aa6d2ec0a073b4 languageName: node linkType: hard @@ -2907,17 +2907,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^8.0.2": - version: 8.0.2 - resolution: "@metamask/json-rpc-engine@npm:8.0.2" - dependencies: - "@metamask/rpc-errors": "npm:^6.2.1" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^8.3.0" - checksum: 10/f088f4b648b9b55875b56e8237853e7282f13302a9db6a1f9bba06314dfd6cd0a23b3d27f8fde05a157b97ebb03b67bc2699ba455c99553dfb2ecccd73ab3474 - languageName: node - linkType: hard - "@metamask/json-rpc-middleware-stream@npm:^8.0.5, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" @@ -3067,7 +3056,7 @@ __metadata: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.4.4" - "@metamask/eth-json-rpc-filters": "npm:^7.0.0" + "@metamask/eth-json-rpc-filters": "npm:^9.0.0" "@metamask/network-controller": "npm:^22.1.1" "@metamask/permission-controller": "npm:^11.0.4" "@metamask/rpc-errors": "npm:^7.0.1" @@ -3481,16 +3470,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/rpc-errors@npm:^6.2.1": - version: 6.3.1 - resolution: "@metamask/rpc-errors@npm:6.3.1" - dependencies: - "@metamask/utils": "npm:^9.0.0" - fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf - languageName: node - linkType: hard - "@metamask/rpc-errors@npm:^7.0.0, @metamask/rpc-errors@npm:^7.0.1": version: 7.0.1 resolution: "@metamask/rpc-errors@npm:7.0.1" @@ -3818,7 +3797,7 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^8.2.0, @metamask/utils@npm:^8.3.0": +"@metamask/utils@npm:^8.2.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" dependencies: From f5ff8951bd70f1ac1010dfe65adc0f84d475e2a3 Mon Sep 17 00:00:00 2001 From: Desi McAdam Date: Sat, 7 Dec 2024 13:07:51 -0700 Subject: [PATCH 06/23] Add label for use in github project auto add workflow (#5042) ## Explanation We have a github project setup to track client controller upgrade issues that are created by this github action. Github projects workflow is not working for auto adding solely based on title match and the options for what to match on are very limited. Labels are one way we can do that. This PR adds a label that we can trigger the auto add workflow on. ## References ## Changelog n/a ## Checklist n/a --- .github/workflows/create-update-issues.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-update-issues.yaml b/.github/workflows/create-update-issues.yaml index 6533d0ef1a..b0189812f0 100644 --- a/.github/workflows/create-update-issues.yaml +++ b/.github/workflows/create-update-issues.yaml @@ -36,8 +36,8 @@ jobs: if [[ $version == *.0.0 ]]; then # Fetch responsible team from file teams=$(jq -r --arg key "$package_name" '.[$key]' teams.json) - gh issue create --title "Update ${package_name} to version ${version}" --body "Please update ${package_name} to version ${version}" --repo "MetaMask/metamask-extension" --label "$teams" - gh issue create --title "Update ${package_name} to version ${version}" --body "Please update ${package_name} to version ${version}" --repo "MetaMask/metamask-mobile" --label "$teams" + gh issue create --title "Update ${package_name} to version ${version}" --body "Please update ${package_name} to version ${version}" --repo "MetaMask/metamask-extension" --label "$teams, client-controller-update" + gh issue create --title "Update ${package_name} to version ${version}" --body "Please update ${package_name} to version ${version}" --repo "MetaMask/metamask-mobile" --label "$teams, client-controller-update" fi fi done From 5d9f45cfa6a2bf8d774302dc3a0bb7ade7dd7a2e Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 9 Dec 2024 17:22:18 -0330 Subject: [PATCH 07/23] Release 267.0.0 (#5046) ## Explanation Patch release for `@metamask/multichain` ## References N/A ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- package.json | 2 +- packages/multichain/CHANGELOG.md | 9 ++++++++- packages/multichain/package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5069a36d68..38e734899d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "266.0.0", + "version": "267.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/multichain/CHANGELOG.md b/packages/multichain/CHANGELOG.md index e4b95a2d36..68060a640e 100644 --- a/packages/multichain/CHANGELOG.md +++ b/packages/multichain/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.2] + +### Changed + +- Bump `@metamask/eth-json-rpc-filters` from `^7.0.0` to `^9.0.0` ([#5040](https://github.com/MetaMask/core/pull/5040)) + ## [1.1.1] ### Changed @@ -28,7 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#4962](https://github.com/MetaMask/core/pull/4962)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.2...HEAD +[1.1.2]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.1...@metamask/multichain@1.1.2 [1.1.1]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.0...@metamask/multichain@1.1.1 [1.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.0.0...@metamask/multichain@1.1.0 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/multichain@1.0.0 diff --git a/packages/multichain/package.json b/packages/multichain/package.json index c7014cbe9b..6ce6e734de 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain", - "version": "1.1.1", + "version": "1.1.2", "description": "Provides types, helpers, adapters, and wrappers for facilitating CAIP Multichain sessions", "keywords": [ "MetaMask", From 1fd0d3a4a7d0538cb2a945b0ee3c78fb09e53937 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 9 Dec 2024 19:38:42 -0330 Subject: [PATCH 08/23] Release 268.0.0 (#5048) ## Explanation Dependency update patches for 3 packages ## References N/A ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- package.json | 2 +- packages/keyring-controller/package.json | 2 +- packages/message-manager/CHANGELOG.md | 9 ++++++++- packages/message-manager/package.json | 2 +- packages/queued-request-controller/CHANGELOG.md | 9 ++++++++- packages/queued-request-controller/package.json | 4 ++-- packages/selected-network-controller/CHANGELOG.md | 9 ++++++++- packages/selected-network-controller/package.json | 2 +- yarn.lock | 8 ++++---- 9 files changed, 34 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 38e734899d..390d2d6879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "267.0.0", + "version": "268.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 3979b55b96..73a80303ba 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -55,7 +55,7 @@ "@metamask/eth-sig-util": "^8.0.0", "@metamask/eth-simple-keyring": "^6.0.5", "@metamask/keyring-api": "^10.1.0", - "@metamask/message-manager": "^11.0.2", + "@metamask/message-manager": "^11.0.3", "@metamask/utils": "^10.0.0", "async-mutex": "^0.5.0", "ethereumjs-wallet": "^1.0.1", diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md index b136cf8f23..686f9553f2 100644 --- a/packages/message-manager/CHANGELOG.md +++ b/packages/message-manager/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.0.3] + +### Changed + +- Bump `jsonschema` from `^1.2.4` to `^1.4.1` ([#4998](https://github.com/MetaMask/core/pull/4998), [#5027](https://github.com/MetaMask/core/pull/5027)) + ## [11.0.2] ### Changed @@ -340,7 +346,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.3...HEAD +[11.0.3]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.2...@metamask/message-manager@11.0.3 [11.0.2]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.1...@metamask/message-manager@11.0.2 [11.0.1]: https://github.com/MetaMask/core/compare/@metamask/message-manager@11.0.0...@metamask/message-manager@11.0.1 [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/message-manager@10.1.1...@metamask/message-manager@11.0.0 diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json index 8f3215bbe1..d2eb85bb8a 100644 --- a/packages/message-manager/package.json +++ b/packages/message-manager/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/message-manager", - "version": "11.0.2", + "version": "11.0.3", "description": "Stores and manages interactions with signing requests", "keywords": [ "MetaMask", diff --git a/packages/queued-request-controller/CHANGELOG.md b/packages/queued-request-controller/CHANGELOG.md index ccc6af36dc..b7de985aae 100644 --- a/packages/queued-request-controller/CHANGELOG.md +++ b/packages/queued-request-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.2] + +### Changed + +- Bump `swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ([#5036](https://github.com/MetaMask/core/pull/5036)) + ## [8.0.1] ### Changed @@ -314,7 +320,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@8.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@8.0.2...HEAD +[8.0.2]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@8.0.1...@metamask/queued-request-controller@8.0.2 [8.0.1]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@8.0.0...@metamask/queued-request-controller@8.0.1 [8.0.0]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.1...@metamask/queued-request-controller@8.0.0 [7.0.1]: https://github.com/MetaMask/core/compare/@metamask/queued-request-controller@7.0.0...@metamask/queued-request-controller@7.0.1 diff --git a/packages/queued-request-controller/package.json b/packages/queued-request-controller/package.json index 96ec49ce15..cf9dac1d1d 100644 --- a/packages/queued-request-controller/package.json +++ b/packages/queued-request-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/queued-request-controller", - "version": "8.0.1", + "version": "8.0.2", "description": "Includes a controller and middleware that implements a request queue", "keywords": [ "MetaMask", @@ -57,7 +57,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.1.1", - "@metamask/selected-network-controller": "^20.0.1", + "@metamask/selected-network-controller": "^20.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/packages/selected-network-controller/CHANGELOG.md b/packages/selected-network-controller/CHANGELOG.md index ce6b09f998..5ea04cc5e4 100644 --- a/packages/selected-network-controller/CHANGELOG.md +++ b/packages/selected-network-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [20.0.2] + +### Changed + +- Bump `swappable-obj-proxy` from `^2.2.0` to `^2.3.0` ([#5036](https://github.com/MetaMask/core/pull/5036)) + ## [20.0.1] ### Fixed @@ -317,7 +323,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial Release ([#1643](https://github.com/MetaMask/core/pull/1643)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@20.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@20.0.2...HEAD +[20.0.2]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@20.0.1...@metamask/selected-network-controller@20.0.2 [20.0.1]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@20.0.0...@metamask/selected-network-controller@20.0.1 [20.0.0]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@19.0.0...@metamask/selected-network-controller@20.0.0 [19.0.0]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@18.0.2...@metamask/selected-network-controller@19.0.0 diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index 5701412dea..865a6603bb 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/selected-network-controller", - "version": "20.0.1", + "version": "20.0.2", "description": "Provides an interface to the currently selected networkClientId for a given domain", "keywords": [ "MetaMask", diff --git a/yarn.lock b/yarn.lock index 625b48a106..5662cf9c72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2978,7 +2978,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/eth-simple-keyring": "npm:^6.0.5" "@metamask/keyring-api": "npm:^10.1.0" - "@metamask/message-manager": "npm:^11.0.2" + "@metamask/message-manager": "npm:^11.0.3" "@metamask/providers": "npm:^18.1.1" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/utils": "npm:^10.0.0" @@ -3020,7 +3020,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/message-manager@npm:^11.0.2, @metamask/message-manager@workspace:packages/message-manager": +"@metamask/message-manager@npm:^11.0.3, @metamask/message-manager@workspace:packages/message-manager": version: 0.0.0-use.local resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: @@ -3411,7 +3411,7 @@ __metadata: "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/network-controller": "npm:^22.1.1" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/selected-network-controller": "npm:^20.0.1" + "@metamask/selected-network-controller": "npm:^20.0.2" "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3497,7 +3497,7 @@ __metadata: languageName: node linkType: hard -"@metamask/selected-network-controller@npm:^20.0.1, @metamask/selected-network-controller@workspace:packages/selected-network-controller": +"@metamask/selected-network-controller@npm:^20.0.2, @metamask/selected-network-controller@workspace:packages/selected-network-controller": version: 0.0.0-use.local resolution: "@metamask/selected-network-controller@workspace:packages/selected-network-controller" dependencies: From 22516ea519c1886206fcbf5dde8ae8cd18b48c72 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Wed, 11 Dec 2024 13:47:14 +0100 Subject: [PATCH 09/23] Release 269.0.0 (#5054) ## Explanation This is a RC for v269.0.0. See changelog for more details @metamask/profile-sync-controller@3.1.0 ## References ## Changelog ```ms ### Changed - Bump `@metamask/profile-sync-controller` from `^3.0.0` to `^3.1.0` ([#5054](https://github.com/MetaMask/core/pull/5054)) ``` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- package.json | 2 +- .../notification-services-controller/package.json | 2 +- packages/profile-sync-controller/CHANGELOG.md | 15 ++++++++++++++- packages/profile-sync-controller/package.json | 2 +- yarn.lock | 4 ++-- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 390d2d6879..51e285b860 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "268.0.0", + "version": "269.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index 14c32397a9..bd7ea3b8a4 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -113,7 +113,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.1", - "@metamask/profile-sync-controller": "^3.0.0", + "@metamask/profile-sync-controller": "^3.1.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index a9e6f4d90f..c8101d0897 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1.0] + +### Changed + +- Revamp user storage encryption process ([#4981](https://github.com/MetaMask/core/pull/4981)) + - Stop using a random salt when generating scrypt keys and use a shared one + - Re-encrypt data fetched by `getUserStorageAllFeatureEntries` and `getUserStorage` with the shared salt if fetched entries were encrypted with random salts + +### Fixed + +- Remove `#assertLoggedIn()` assertion when signing out a user, ensuring `performSignOut` does not error when a user is already signed out ([#5013](https://github.com/MetaMask/core/pull/5013)) + ## [3.0.0] ### Added @@ -358,7 +370,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.0...HEAD +[3.1.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.0.0...@metamask/profile-sync-controller@3.1.0 [3.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@2.0.0...@metamask/profile-sync-controller@3.0.0 [2.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@1.0.2...@metamask/profile-sync-controller@2.0.0 [1.0.2]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@1.0.1...@metamask/profile-sync-controller@1.0.2 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 00560e1c3c..f55fda8da6 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/profile-sync-controller", - "version": "3.0.0", + "version": "3.1.0", "description": "The profile sync helps developers synchronize data across multiple clients and devices in a privacy-preserving way. All data saved in the user storage database is encrypted client-side to preserve privacy. The user storage provides a modular design, giving developers the flexibility to construct and manage their storage spaces in a way that best suits their needs", "keywords": [ "MetaMask", diff --git a/yarn.lock b/yarn.lock index 5662cf9c72..11e7522926 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3157,7 +3157,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/keyring-controller": "npm:^19.0.1" - "@metamask/profile-sync-controller": "npm:^3.0.0" + "@metamask/profile-sync-controller": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -3338,7 +3338,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^3.0.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^3.1.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: From 058534ee12dcbda3a45e1f539e4a7743b88bcfc9 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 11 Dec 2024 15:11:06 +0100 Subject: [PATCH 10/23] refactor: use new `@metamask/keyring-api` layout (split packages) (#4695) ## Explanation The `keyring-api` has been split out into multiple smaller packages, this introduce some breaking changes since some exports have been moved elsewhere. This was done to reduce the number of required dependencies for the `keyring-api`. The `InternalAccount` type (widely used internally by some controllers) has been moved to a new package: `@metamask/keyring-internal-api`. ## References Relates to: - https://github.com/MetaMask/accounts/pull/24 Basic testing done through the CI + preview builds: - https://github.com/MetaMask/metamask-extension/pull/28861 ## Changelog ### `@metamask/keyring-controller` - **CHANGED**: Remove use of peer dependency `@metamask/providers` - **CHANGED**: Bump `@metamask/keyring-api` to `^12.0.0` - **CHANGED**: Use `@metamask/keyring-internal-api@^1.0.0` > Even if we are bumping the major of the `keyring-api`, this is not breaking since all > types from the split are compatible. The only difference now here is the `Keyring` > interface which has a new optional method `listAccountTransactions`, optional, thus > non-breaking (and this type is not used there anyway) ### `@metamask/accounts-controller` - **CHANGED**: Bump `@metamask/keyring-api` to `^12.0.0` - **CHANGED**: Bump `@metamask/eth-snap-keyring` to `^7.0.0` - **CHANGED**: Use `@metamask/keyring-internal-api@^1.0.0` > `eth-snap-keyring` has been bumped to `7.0.0` but this was a mistake since no breaking changes has been introduced there either. > The version `6.0.0` also introduces `ts-bridge` builds, but this is already compatible with this repo, so not breaking here either. ### `@metamask/assets-controllers` - **CHANGED**: Remove use of `@metamask/keyring-api` - **CHANGED**: Use `@metamask/keyring-internal-api@^1.0.0` ### `@metamask/chain-controller` - **CHANGED**: Remove use of `@metamask/keyring-api` - **CHANGED**: Use `@metamask/keyring-internal-api@^1.0.0` - **CHANGED**: Use `@metamask/keyring-utils@^1.0.0` ### `@metamask/profile-sync-controller` - **CHANGED**: Remove use of `@metamask/keyring-api` - **CHANGED**: Use `@metamask/keyring-internal-api@^1.0.0` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- packages/accounts-controller/package.json | 5 +- .../src/AccountsController.test.ts | 10 +- .../src/AccountsController.ts | 2 +- .../accounts-controller/src/tests/mocks.ts | 10 +- packages/assets-controllers/package.json | 4 +- .../src/AccountTrackerController.test.ts | 2 +- .../src/NftController.test.ts | 2 +- .../assets-controllers/src/NftController.ts | 2 +- .../src/TokenDetectionController.test.ts | 2 +- .../src/TokenRatesController.test.ts | 2 +- .../src/TokenRatesController.ts | 2 +- .../src/TokensController.test.ts | 2 +- .../src/TokensController.ts | 2 +- packages/chain-controller/package.json | 3 +- .../src/ChainController.test.ts | 2 +- .../chain-controller/src/ChainController.ts | 2 +- .../chain-controller/src/SnapHandlerClient.ts | 4 +- packages/keyring-controller/package.json | 11 +- .../src/KeyringController.test.ts | 2 +- .../src/KeyringController.ts | 4 +- .../tests/mocks/mockErc4337Keyring.ts | 2 +- .../tests/mocks/mockKeyring.ts | 2 +- packages/profile-sync-controller/package.json | 3 +- .../UserStorageController.test.ts | 2 +- .../user-storage/UserStorageController.ts | 3 +- .../user-storage/__fixtures__/mockAccounts.ts | 3 +- .../user-storage/accounts/user-storage.ts | 2 +- yarn.lock | 112 +++++++++++++----- 28 files changed, 127 insertions(+), 77 deletions(-) diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 108f60b10a..6f224b7150 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -49,8 +49,9 @@ "dependencies": { "@ethereumjs/util": "^8.1.0", "@metamask/base-controller": "^7.0.2", - "@metamask/eth-snap-keyring": "^5.0.1", - "@metamask/keyring-api": "^10.1.0", + "@metamask/eth-snap-keyring": "^7.0.0", + "@metamask/keyring-api": "^12.0.0", + "@metamask/keyring-internal-api": "^1.0.0", "@metamask/snaps-sdk": "^6.7.0", "@metamask/snaps-utils": "^8.3.0", "@metamask/utils": "^10.0.0", diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 4905119e45..e082ea1c38 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,15 +1,15 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import type { - InternalAccount, - InternalAccountType, -} from '@metamask/keyring-api'; import { BtcAccountType, - BtcMethod, EthAccountType, + BtcMethod, EthMethod, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; +import type { + InternalAccount, + InternalAccountType, +} from '@metamask/keyring-internal-api'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; import type { CaipChainId } from '@metamask/utils'; diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 54f2d01614..234d454472 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -5,7 +5,6 @@ import type { } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; import { SnapKeyring } from '@metamask/eth-snap-keyring'; -import type { InternalAccount } from '@metamask/keyring-api'; import { EthAccountType, EthMethod, @@ -19,6 +18,7 @@ import type { KeyringControllerGetAccountsAction, KeyringControllerStateChangeEvent, } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { SnapControllerState, SnapStateChange, diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index b41388fb2f..c5224ab0be 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -1,14 +1,14 @@ -import type { - InternalAccount, - InternalAccountType, -} from '@metamask/keyring-api'; import { BtcAccountType, - BtcMethod, EthAccountType, + BtcMethod, EthMethod, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; +import type { + InternalAccount, + InternalAccountType, +} from '@metamask/keyring-internal-api'; import { v4 } from 'uuid'; export const createMockInternalAccount = ({ diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index cffcd780ad..21425f95ad 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -79,11 +79,10 @@ "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-api": "^10.1.0", "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-internal-api": "^1.0.0", "@metamask/network-controller": "^22.1.1", "@metamask/preferences-controller": "^15.0.1", - "@metamask/providers": "^18.1.1", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", @@ -104,7 +103,6 @@ "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.0", "@metamask/preferences-controller": "^15.0.0", - "@metamask/providers": "^18.1.0", "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" }, "engines": { diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index bd27eecb87..8b82b7b4c6 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -1,6 +1,6 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { query, toChecksumHexAddress } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { type NetworkClientId, type NetworkClientConfiguration, diff --git a/packages/assets-controllers/src/NftController.test.ts b/packages/assets-controllers/src/NftController.test.ts index 2fd003dac4..dc9c9a4a8f 100644 --- a/packages/assets-controllers/src/NftController.test.ts +++ b/packages/assets-controllers/src/NftController.test.ts @@ -20,7 +20,7 @@ import { NFT_API_BASE_URL, InfuraNetworkType, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkClientConfiguration, NetworkClientId, diff --git a/packages/assets-controllers/src/NftController.ts b/packages/assets-controllers/src/NftController.ts index 63c60b53aa..df159b91a9 100644 --- a/packages/assets-controllers/src/NftController.ts +++ b/packages/assets-controllers/src/NftController.ts @@ -26,7 +26,7 @@ import { NFT_API_BASE_URL, NFT_API_VERSION, } from '@metamask/controller-utils'; -import { type InternalAccount } from '@metamask/keyring-api'; +import { type InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkClientId, NetworkControllerGetNetworkClientByIdAction, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 6a1a69e5ea..a084b5a0ac 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -6,8 +6,8 @@ import { convertHexToDecimal, InfuraNetworkType, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; import type { KeyringControllerState } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { getDefaultNetworkControllerState, RpcEndpointType, diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 4648151769..db41deddd4 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -7,7 +7,7 @@ import { toChecksumHexAddress, toHex, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkClientConfiguration, NetworkClientId, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 50ffd583c9..f25702bbf8 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -14,7 +14,7 @@ import { FALL_BACK_VS_CURRENCY, toHex, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, diff --git a/packages/assets-controllers/src/TokensController.test.ts b/packages/assets-controllers/src/TokensController.test.ts index 1f3aa57ea1..250382cd5c 100644 --- a/packages/assets-controllers/src/TokensController.test.ts +++ b/packages/assets-controllers/src/TokensController.test.ts @@ -13,7 +13,7 @@ import { convertHexToDecimal, InfuraNetworkType, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkClientConfiguration, NetworkClientId, diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index feb5293d8c..be24f364b5 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -24,7 +24,7 @@ import { isValidHexAddress, safelyExecute, } from '@metamask/controller-utils'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { abiERC721 } from '@metamask/metamask-eth-abis'; import type { NetworkClientId, diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index 4e5b1a59dd..3e238e9aa0 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -49,7 +49,8 @@ "dependencies": { "@metamask/base-controller": "^7.0.2", "@metamask/chain-api": "^0.1.0", - "@metamask/keyring-api": "^10.1.0", + "@metamask/keyring-internal-api": "^1.0.0", + "@metamask/keyring-utils": "^1.0.0", "@metamask/snaps-controllers": "^9.10.0", "@metamask/snaps-sdk": "^6.7.0", "@metamask/snaps-utils": "^8.3.0", diff --git a/packages/chain-controller/src/ChainController.test.ts b/packages/chain-controller/src/ChainController.test.ts index 734a82a231..8fe506561f 100644 --- a/packages/chain-controller/src/ChainController.test.ts +++ b/packages/chain-controller/src/ChainController.test.ts @@ -1,5 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { SnapId } from '@metamask/snaps-sdk'; import type { AllowedActions, ChainControllerActions } from './ChainController'; diff --git a/packages/chain-controller/src/ChainController.ts b/packages/chain-controller/src/ChainController.ts index 86d32636a9..4e262bd053 100644 --- a/packages/chain-controller/src/ChainController.ts +++ b/packages/chain-controller/src/ChainController.ts @@ -5,7 +5,7 @@ import type { } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; import type { CaipAssetType, BalancesResult, Chain } from '@metamask/chain-api'; -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { HandleSnapRequest as SnapControllerHandleSnapRequestAction } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import type { CaipChainId } from '@metamask/utils'; diff --git a/packages/chain-controller/src/SnapHandlerClient.ts b/packages/chain-controller/src/SnapHandlerClient.ts index fa03a16407..d89d529a69 100644 --- a/packages/chain-controller/src/SnapHandlerClient.ts +++ b/packages/chain-controller/src/SnapHandlerClient.ts @@ -1,4 +1,4 @@ -import type { JsonRpcRequest } from '@metamask/keyring-api/dist/JsonRpcRequest'; +import type { JsonRpcRequest } from '@metamask/keyring-utils'; import type { SnapController } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; @@ -78,7 +78,7 @@ export class SnapHandlerClient { */ constructor({ handler, - // Follow same pattern than for @metamask/keyring-api + // Follow same pattern than for @metamask/keyring-snap-client snapId, origin = 'metamask', }: { diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 73a80303ba..48050fdaeb 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -54,7 +54,8 @@ "@metamask/eth-hd-keyring": "^7.0.4", "@metamask/eth-sig-util": "^8.0.0", "@metamask/eth-simple-keyring": "^6.0.5", - "@metamask/keyring-api": "^10.1.0", + "@metamask/keyring-api": "^12.0.0", + "@metamask/keyring-internal-api": "^1.0.0", "@metamask/message-manager": "^11.0.3", "@metamask/utils": "^10.0.0", "async-mutex": "^0.5.0", @@ -68,7 +69,6 @@ "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/providers": "^18.1.1", "@metamask/scure-bip39": "^2.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", @@ -79,12 +79,7 @@ "typedoc": "^0.24.8", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.2.2", - "uuid": "^8.3.2", - "webextension-polyfill": "^0.12.0" - }, - "peerDependencies": { - "@metamask/providers": "^18.1.0", - "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" + "uuid": "^8.3.2" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index d8e6218725..18a0c2319d 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -12,7 +12,7 @@ import { encrypt, } from '@metamask/eth-sig-util'; import SimpleKeyring from '@metamask/eth-simple-keyring/dist/simple-keyring'; -import type { EthKeyring } from '@metamask/keyring-api'; +import type { EthKeyring } from '@metamask/keyring-internal-api'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import type { KeyringClass } from '@metamask/utils'; import { diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 375397fd60..06ed76bca3 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -11,13 +11,13 @@ import HDKeyring from '@metamask/eth-hd-keyring'; import { normalize as ethNormalize } from '@metamask/eth-sig-util'; import SimpleKeyring from '@metamask/eth-simple-keyring'; import type { + KeyringExecutionContext, EthBaseTransaction, EthBaseUserOperation, - EthKeyring, EthUserOperation, EthUserOperationPatch, - KeyringExecutionContext, } from '@metamask/keyring-api'; +import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { PersonalMessageParams, TypedMessageParams, diff --git a/packages/keyring-controller/tests/mocks/mockErc4337Keyring.ts b/packages/keyring-controller/tests/mocks/mockErc4337Keyring.ts index 2838ea0b55..6d3f0d3e68 100644 --- a/packages/keyring-controller/tests/mocks/mockErc4337Keyring.ts +++ b/packages/keyring-controller/tests/mocks/mockErc4337Keyring.ts @@ -1,4 +1,4 @@ -import type { EthKeyring } from '@metamask/keyring-api'; +import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { Hex, Json } from '@metamask/utils'; export class MockErc4337Keyring implements EthKeyring { diff --git a/packages/keyring-controller/tests/mocks/mockKeyring.ts b/packages/keyring-controller/tests/mocks/mockKeyring.ts index 9ce4b50caf..38770fbcbd 100644 --- a/packages/keyring-controller/tests/mocks/mockKeyring.ts +++ b/packages/keyring-controller/tests/mocks/mockKeyring.ts @@ -1,4 +1,4 @@ -import type { EthKeyring } from '@metamask/keyring-api'; +import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { Json, Hex } from '@metamask/utils'; export class MockKeyring implements EthKeyring { diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index f55fda8da6..051f755b61 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -101,7 +101,7 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.2", - "@metamask/keyring-api": "^10.1.0", + "@metamask/keyring-api": "^12.0.0", "@metamask/keyring-controller": "^19.0.1", "@metamask/network-controller": "^22.1.1", "@metamask/snaps-sdk": "^6.7.0", @@ -117,6 +117,7 @@ "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/accounts-controller": "^20.0.1", "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-internal-api": "^1.0.0", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.10.0", "@types/jest": "^27.4.1", diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index feb2c8d1d3..300d0e04d4 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -1,4 +1,4 @@ -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type nock from 'nock'; import encryption, { createSHA256Hash } from '../../shared/encryption'; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index c698b101bc..74a3eab880 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -11,7 +11,7 @@ import type { StateMetadata, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; -import { type InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { isEvmAccountType } from '@metamask/keyring-api'; import { type KeyringControllerGetStateAction, type KeyringControllerLockEvent, @@ -19,6 +19,7 @@ import { type KeyringControllerAddNewAccountAction, KeyringTypes, } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkControllerAddNetworkAction, NetworkControllerGetStateAction, diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts index 5103853d44..8664fcd6cc 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockAccounts.ts @@ -1,5 +1,6 @@ -import { EthAccountType, type InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { LOCALIZED_DEFAULT_ACCOUNT_NAMES } from '../accounts/constants'; import { mapInternalAccountToUserStorageAccount } from '../accounts/user-storage'; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts b/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts index c02ef96fad..374063a20f 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/accounts/user-storage.ts @@ -1,4 +1,4 @@ -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { USER_STORAGE_VERSION_KEY, diff --git a/yarn.lock b/yarn.lock index 11e7522926..808c2159ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2041,9 +2041,10 @@ __metadata: "@ethereumjs/util": "npm:^8.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/eth-snap-keyring": "npm:^5.0.1" - "@metamask/keyring-api": "npm:^10.1.0" + "@metamask/eth-snap-keyring": "npm:^7.0.0" + "@metamask/keyring-api": "npm:^12.0.0" "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -2161,13 +2162,12 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" - "@metamask/keyring-api": "npm:^10.1.0" "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/preferences-controller": "npm:^15.0.1" - "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -2199,7 +2199,6 @@ __metadata: "@metamask/keyring-controller": ^19.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^15.0.0 - "@metamask/providers": ^18.1.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 languageName: unknown linkType: soft @@ -2298,7 +2297,8 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/chain-api": "npm:^0.1.0" - "@metamask/keyring-api": "npm:^10.1.0" + "@metamask/keyring-internal-api": "npm:^1.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" @@ -2671,22 +2671,27 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^5.0.1": - version: 5.0.1 - resolution: "@metamask/eth-snap-keyring@npm:5.0.1" +"@metamask/eth-snap-keyring@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/eth-snap-keyring@npm:7.0.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-internal-api": "npm:^1.0.0" + "@metamask/keyring-internal-snap-client": "npm:^1.0.0" "@metamask/snaps-controllers": "npm:^9.10.0" "@metamask/snaps-sdk": "npm:^6.7.0" "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" + "@metamask/utils": "npm:^9.3.0" "@types/uuid": "npm:^9.0.8" uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-api": ^10.1.0 - checksum: 10/4d9d700b7c2ecc1b17e92f716f7aeb04bbd03836601b5d37f639bed7fba4d5f00bafadf5359d2416c319cdf18eb2f9417c7353654737af87a6e8579d5e5bab79 + "@metamask/keyring-api": ^12.0.0 + "@metamask/providers": ^18.1.0 + checksum: 10/7a82cd2c19204776d31e29716844ac6f304ce4b136b36728be8e7b19ac2be6b46d0c72cc9707c48669a7a342994ce401aafbfe3f0b47769748ab86ae2169cbbd languageName: node linkType: hard @@ -2943,20 +2948,15 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^10.1.0": - version: 10.1.0 - resolution: "@metamask/keyring-api@npm:10.1.0" +"@metamask/keyring-api@npm:^12.0.0": + version: 12.0.0 + resolution: "@metamask/keyring-api@npm:12.0.0" dependencies: - "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/keyring-utils": "npm:^1.0.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" - "@types/uuid": "npm:^9.0.8" + "@metamask/utils": "npm:^9.3.0" bech32: "npm:^2.0.0" - uuid: "npm:^9.0.1" - webextension-polyfill: "npm:^0.12.0" - peerDependencies: - "@metamask/providers": ^18.1.0 - checksum: 10/de22b9f5f3aecc290210fa78161e157aa8358f8dad421a093c9f6dbe35c4755067472a732f10d1ddbfba789e871c64edd8ea1c4c7316a392b214a187efd46ebe + checksum: 10/ba8b75c55d3fcb9f8b52c58ff141cba81f7c416c3fa684e089965717ea129d50e8df7a73e7ab1c96eaf59d70b6e2dd8a618434939b75ef0d3402b547b5196877 languageName: node linkType: hard @@ -2977,9 +2977,9 @@ __metadata: "@metamask/eth-hd-keyring": "npm:^7.0.4" "@metamask/eth-sig-util": "npm:^8.0.0" "@metamask/eth-simple-keyring": "npm:^6.0.5" - "@metamask/keyring-api": "npm:^10.1.0" + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/message-manager": "npm:^11.0.3" - "@metamask/providers": "npm:^18.1.1" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -2995,12 +2995,63 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" uuid: "npm:^8.3.2" + languageName: unknown + linkType: soft + +"@metamask/keyring-internal-api@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/keyring-internal-api@npm:1.0.0" + dependencies: + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.3.0" + checksum: 10/dd0fff93ddfdce008f1db82d404bd040d09840413723c831819d3a7f4c2819a4303657e4acd7578cfd22bd05ad9c7aa563fc88f13f2f06999e2325ada71b824c + languageName: node + linkType: hard + +"@metamask/keyring-internal-snap-client@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/keyring-internal-snap-client@npm:1.0.0" + dependencies: + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-snap-client": "npm:^1.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/snaps-controllers": "npm:^9.10.0" + "@metamask/snaps-sdk": "npm:^6.7.0" + "@metamask/snaps-utils": "npm:^8.3.0" webextension-polyfill: "npm:^0.12.0" peerDependencies: "@metamask/providers": ^18.1.0 - webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - languageName: unknown - linkType: soft + checksum: 10/4c02429235bf5b3609fe2d728f0f976fc87cc276483c092d155b181eeb1f167953a96226c4f6b64f6700084f8a66e5b02944ba0accf201e3514e83df205389e1 + languageName: node + linkType: hard + +"@metamask/keyring-snap-client@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/keyring-snap-client@npm:1.0.0" + dependencies: + "@metamask/keyring-api": "npm:^12.0.0" + "@metamask/keyring-utils": "npm:^1.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@types/uuid": "npm:^9.0.8" + uuid: "npm:^9.0.1" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/providers": ^18.1.0 + checksum: 10/7b3ee4ab6b39f8e06d55dee2c29f778eeb2eeb8bb311eccaab07d1f8a855fa920bf52e78bd2be0f3ddcb66dc475282d740de0cc7337ccd99e956302a706d76a0 + languageName: node + linkType: hard + +"@metamask/keyring-utils@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/keyring-utils@npm:1.0.0" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^9.3.0" + checksum: 10/f74f7343a7154b029e0fa4c25735c589eba4dc25a9e323d43b7c733ce5dbb23ce603a4f02aac455163993649ceeaf714b8b843985ba7a9cb00b926b3b8dc6b51 + languageName: node + linkType: hard "@metamask/logging-controller@npm:^6.0.3, @metamask/logging-controller@workspace:packages/logging-controller": version: 0.0.0-use.local @@ -3347,8 +3398,9 @@ __metadata: "@metamask/accounts-controller": "npm:^20.0.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/keyring-api": "npm:^10.1.0" + "@metamask/keyring-api": "npm:^12.0.0" "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/network-controller": "npm:^22.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" @@ -3814,7 +3866,7 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1, @metamask/utils@npm:^9.3.0": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" dependencies: From 6dab1e67e04e1feacbd05487057946703a2ed8f8 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 11 Dec 2024 16:35:43 +0100 Subject: [PATCH 11/23] fix(assets-controllers): fix missing peer dependency (#5057) ## Explanation This peer dependency has been wrongly removed with my previous PR. I'm just re-adding it. ## References - https://github.com/MetaMask/core/pull/4695 ## Changelog N/A (will be unchanged for the next release) ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/assets-controllers/package.json | 2 ++ yarn.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 21425f95ad..f1a5b5b451 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -83,6 +83,7 @@ "@metamask/keyring-internal-api": "^1.0.0", "@metamask/network-controller": "^22.1.1", "@metamask/preferences-controller": "^15.0.1", + "@metamask/providers": "^18.1.1", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.191", "@types/node": "^16.18.54", @@ -103,6 +104,7 @@ "@metamask/keyring-controller": "^19.0.0", "@metamask/network-controller": "^22.0.0", "@metamask/preferences-controller": "^15.0.0", + "@metamask/providers": "^18.1.0", "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" }, "engines": { diff --git a/yarn.lock b/yarn.lock index 808c2159ac..f35becfecb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2168,6 +2168,7 @@ __metadata: "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/preferences-controller": "npm:^15.0.1" + "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/bn.js": "npm:^5.1.5" @@ -2199,6 +2200,7 @@ __metadata: "@metamask/keyring-controller": ^19.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^15.0.0 + "@metamask/providers": ^18.1.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 languageName: unknown linkType: soft From 811ee6a49dc27ab1de10d212294813b663124380 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 11 Dec 2024 19:55:50 +0100 Subject: [PATCH 12/23] chore: fix typo in CODEOWNERS (assets-controller -> assets-controllers) (#5061) ## Explanation Found the same typo than last time, but on another section of the CODEOWNERS. Spotted while doing this release: - https://github.com/MetaMask/core/pull/5058 ## References Similar to: - https://github.com/MetaMask/core/pull/5029 ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 539480db30..df7a5be821 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -68,8 +68,8 @@ /packages/announcement-controller/CHANGELOG.md @MetaMask/wallet-ux @MetaMask/wallet-framework-engineers /packages/approval-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers /packages/approval-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/wallet-framework-engineers -/packages/assets-controller/package.json @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers -/packages/assets-controller/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers +/packages/assets-controllers/package.json @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers +/packages/assets-controllers/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/wallet-framework-engineers /packages/chain-controller/package.json @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/chain-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/ens-controller/package.json @MetaMask/confirmations @MetaMask/wallet-framework-engineers From 5cba6b166bda1c4b91f743b9a9a8d4c673418f38 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 11 Dec 2024 20:00:43 +0100 Subject: [PATCH 13/23] Release 270.0.0 (#5058) ## Explanation Use the new Keyring API layout in related controllers. Some peer dependencies have been removed on the `keyring-controller` (but they remain here for the `accounts-controller` for now). ## References N/A ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Elliot Winkler --- package.json | 2 +- packages/accounts-controller/CHANGELOG.md | 13 ++++++++- packages/accounts-controller/package.json | 4 +-- packages/assets-controllers/CHANGELOG.md | 12 +++++++- packages/assets-controllers/package.json | 6 ++-- packages/chain-controller/CHANGELOG.md | 13 ++++++++- packages/chain-controller/package.json | 2 +- packages/keyring-controller/CHANGELOG.md | 13 ++++++++- packages/keyring-controller/package.json | 2 +- .../package.json | 4 +-- packages/preferences-controller/package.json | 2 +- packages/profile-sync-controller/CHANGELOG.md | 11 +++++++- packages/profile-sync-controller/package.json | 6 ++-- packages/signature-controller/package.json | 2 +- packages/transaction-controller/package.json | 2 +- .../user-operation-controller/package.json | 2 +- yarn.lock | 28 +++++++++---------- 17 files changed, 88 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 51e285b860..52d0b51f10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "269.0.0", + "version": "270.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index a267a37fd8..4fbb97c20e 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [20.0.2] + +### Changed + +- Use new `@metamask/keyring-internal-api@^1.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - This package has been split out from the Keyring API. +- Bump `@metamask/keyring-api` from `^10.1.0` to `^12.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) +- Bump `@metamask/eth-snap-keyring` from `^5.0.1` to `^7.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - ESM/CommonJS support. + ## [20.0.1] ### Fixed @@ -368,7 +378,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#1637](https://github.com/MetaMask/core/pull/1637)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@20.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@20.0.2...HEAD +[20.0.2]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@20.0.1...@metamask/accounts-controller@20.0.2 [20.0.1]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@20.0.0...@metamask/accounts-controller@20.0.1 [20.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@19.0.0...@metamask/accounts-controller@20.0.0 [19.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@18.2.3...@metamask/accounts-controller@19.0.0 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 6f224b7150..f5e5c16507 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "20.0.1", + "version": "20.0.2", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -62,7 +62,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.10.0", "@types/jest": "^27.4.1", diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index aae6a321e6..a8bdc7582e 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [45.1.2] + +### Changed + +- Remove use of `@metamask/keyring-api` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - `@metamask/providers` and `webextension-polyfill` peer dependencies are no longer required. +- Use new `@metamask/keyring-internal-api@^1.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - This package has been split out from the Keyring API. Its types are compatible with the `@metamask/keyring-api` package used previously. + ## [45.1.1] ### Changed @@ -1291,7 +1300,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@45.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@45.1.2...HEAD +[45.1.2]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@45.1.1...@metamask/assets-controllers@45.1.2 [45.1.1]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@45.1.0...@metamask/assets-controllers@45.1.1 [45.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@45.0.0...@metamask/assets-controllers@45.1.0 [45.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@44.1.0...@metamask/assets-controllers@45.0.0 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index f1a5b5b451..820b753f07 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "45.1.1", + "version": "45.1.2", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", @@ -75,11 +75,11 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^20.0.1", + "@metamask/accounts-controller": "^20.0.2", "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/ethjs-provider-http": "^0.3.0", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@metamask/keyring-internal-api": "^1.0.0", "@metamask/network-controller": "^22.1.1", "@metamask/preferences-controller": "^15.0.1", diff --git a/packages/chain-controller/CHANGELOG.md b/packages/chain-controller/CHANGELOG.md index 70804cb498..93b165cbe5 100644 --- a/packages/chain-controller/CHANGELOG.md +++ b/packages/chain-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] + +### Changed + +- Remove use of `@metamask/keyring-api` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - `@metamask/providers` and `webextension-polyfill` peer dependencies are no longer required. +- Use new `@metamask/keyring-internal-api@^1.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - This package has been split out from the Keyring API. Its types are compatible with the `@metamask/keyring-api` package used previously. +- Use new `@metamask/keyring-utils@^1.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + ## [0.2.1] ### Fixed @@ -79,7 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.2.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.2.2...HEAD +[0.2.2]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.2.1...@metamask/chain-controller@0.2.2 [0.2.1]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.2.0...@metamask/chain-controller@0.2.1 [0.2.0]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.1.3...@metamask/chain-controller@0.2.0 [0.1.3]: https://github.com/MetaMask/core/compare/@metamask/chain-controller@0.1.2...@metamask/chain-controller@0.1.3 diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index 3e238e9aa0..69311fedd4 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/chain-controller", - "version": "0.2.1", + "version": "0.2.2", "description": "Manages chain-agnostic providers", "keywords": [ "MetaMask", diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 8a33eeafa3..fbd84db795 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [19.0.2] + +### Changed + +- Remove use of `@metamask/keyring-api` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - `@metamask/providers` and `webextension-polyfill` peer depedencies are no longer required. +- Use new `@metamask/keyring-internal-api@^1.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + - This package has been split out from the Keyring API. Its types are compatible with the `@metamask/keyring-api` package used previously. +- Bump `@metamask/message-manager` from `^11.0.2` to `^11.0.3` ([#5048](https://github.com/MetaMask/core/pull/5048)) + ## [19.0.1] ### Changed @@ -608,7 +618,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.2...HEAD +[19.0.2]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.1...@metamask/keyring-controller@19.0.2 [19.0.1]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@19.0.0...@metamask/keyring-controller@19.0.1 [19.0.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@18.0.0...@metamask/keyring-controller@19.0.0 [18.0.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@17.3.1...@metamask/keyring-controller@18.0.0 diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 48050fdaeb..31325cc108 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-controller", - "version": "19.0.1", + "version": "19.0.2", "description": "Stores identities seen in the wallet and manages interactions such as signing", "keywords": [ "MetaMask", diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index bd7ea3b8a4..d357cd58b9 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -112,8 +112,8 @@ "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.1", - "@metamask/profile-sync-controller": "^3.1.0", + "@metamask/keyring-controller": "^19.0.2", + "@metamask/profile-sync-controller": "^3.1.1", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index 6b7fea5870..1b7fbee4d8 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -52,7 +52,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index c8101d0897..74351c180e 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1.1] + +### Changed + +- Use new `@metamask/keyring-internal-api@^1.0.0`( [#4695](https://github.com/MetaMask/core/pull/4695)) + - This package has been split out from the Keyring API. +- Bump `@metamask/keyring-api` from `^10.1.0` to `^12.0.0` ([#4695](https://github.com/MetaMask/core/pull/4695)) + ## [3.1.0] ### Changed @@ -370,7 +378,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.1...HEAD +[3.1.1]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.1.0...@metamask/profile-sync-controller@3.1.1 [3.1.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@3.0.0...@metamask/profile-sync-controller@3.1.0 [3.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@2.0.0...@metamask/profile-sync-controller@3.0.0 [2.0.0]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@1.0.2...@metamask/profile-sync-controller@2.0.0 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 051f755b61..6fd891ea49 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/profile-sync-controller", - "version": "3.1.0", + "version": "3.1.1", "description": "The profile sync helps developers synchronize data across multiple clients and devices in a privacy-preserving way. All data saved in the user storage database is encrypted client-side to preserve privacy. The user storage provides a modular design, giving developers the flexibility to construct and manage their storage spaces in a way that best suits their needs", "keywords": [ "MetaMask", @@ -102,7 +102,7 @@ "dependencies": { "@metamask/base-controller": "^7.0.2", "@metamask/keyring-api": "^12.0.0", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@metamask/network-controller": "^22.1.1", "@metamask/snaps-sdk": "^6.7.0", "@metamask/snaps-utils": "^8.3.0", @@ -115,7 +115,7 @@ "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@lavamoat/preinstall-always-fail": "^2.1.0", - "@metamask/accounts-controller": "^20.0.1", + "@metamask/accounts-controller": "^20.0.2", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-internal-api": "^1.0.0", "@metamask/providers": "^18.1.1", diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index 6a312ede66..74373542e4 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -58,7 +58,7 @@ "devDependencies": { "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@metamask/logging-controller": "^6.0.3", "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 4b0d475851..711475c70f 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "@babel/runtime": "^7.23.9", - "@metamask/accounts-controller": "^20.0.1", + "@metamask/accounts-controller": "^20.0.2", "@metamask/approval-controller": "^7.1.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", diff --git a/packages/user-operation-controller/package.json b/packages/user-operation-controller/package.json index 3d790d952f..8250510b02 100644 --- a/packages/user-operation-controller/package.json +++ b/packages/user-operation-controller/package.json @@ -65,7 +65,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-block-tracker": "^11.0.3", "@metamask/gas-fee-controller": "^22.0.2", - "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-controller": "^19.0.2", "@metamask/network-controller": "^22.1.1", "@metamask/transaction-controller": "^42.0.0", "@types/jest": "^27.4.1", diff --git a/yarn.lock b/yarn.lock index f35becfecb..cbc8b3365e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2034,7 +2034,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^20.0.1, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^20.0.2, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2043,7 +2043,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/eth-snap-keyring": "npm:^7.0.0" "@metamask/keyring-api": "npm:^12.0.0" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.10.0" @@ -2154,7 +2154,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/accounts-controller": "npm:^20.0.1" + "@metamask/accounts-controller": "npm:^20.0.2" "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" @@ -2162,7 +2162,7 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-provider-http": "npm:^0.3.0" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^22.1.1" @@ -2962,7 +2962,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.1, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.2, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3209,8 +3209,8 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" - "@metamask/keyring-controller": "npm:^19.0.1" - "@metamask/profile-sync-controller": "npm:^3.1.0" + "@metamask/keyring-controller": "npm:^19.0.2" + "@metamask/profile-sync-controller": "npm:^3.1.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -3377,7 +3377,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3391,17 +3391,17 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^3.1.0, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^3.1.1, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask/accounts-controller": "npm:^20.0.1" + "@metamask/accounts-controller": "npm:^20.0.2" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" "@metamask/keyring-api": "npm:^12.0.0" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/keyring-internal-api": "npm:^1.0.0" "@metamask/network-controller": "npm:^22.1.1" "@metamask/providers": "npm:^18.1.1" @@ -3588,7 +3588,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.2" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-sig-util": "npm:^8.0.0" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/logging-controller": "npm:^6.0.3" "@metamask/network-controller": "npm:^22.1.1" "@metamask/utils": "npm:^10.0.0" @@ -3752,7 +3752,7 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^20.0.1" + "@metamask/accounts-controller": "npm:^20.0.2" "@metamask/approval-controller": "npm:^7.1.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.2" @@ -3806,7 +3806,7 @@ __metadata: "@metamask/eth-block-tracker": "npm:^11.0.3" "@metamask/eth-query": "npm:^4.0.0" "@metamask/gas-fee-controller": "npm:^22.0.2" - "@metamask/keyring-controller": "npm:^19.0.1" + "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/network-controller": "npm:^22.1.1" "@metamask/polling-controller": "npm:^12.0.2" "@metamask/rpc-errors": "npm:^7.0.1" From 742e7d83ca4470cee04d3e5cede0f7eaf6744c34 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Thu, 12 Dec 2024 16:16:01 +0000 Subject: [PATCH 14/23] feat(3742): Feature Flag Values with Scope Based on threshold (#5051) ## Explanation This PR added ability for @metamask/remote-feature-flag-controller to take in a new param `metametricsId` and applied to the featureFlag processing. Furthermore, a threshold will be generated using a simple function, which generates a deterministic random group number between 0 and 1 from `metametricsId`. If encounters a flag from api with scope as `type: "threshold"`, controller will drill down to return the value that's matching the threshold generated based on `metametricsId`. Note: `metametricsId` will always be provided from client, a random SHA-256 hash will be used as fallback ## References Fixes https://github.com/MetaMask/MetaMask-planning/issues/3742 ## Changelog ### `@metamask/remote-feature-flag-controller` - **ADDED**: Added support for threshold-based feature flag scoping ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../CHANGELOG.md | 11 ++- .../package.json | 3 +- .../remote-feature-flag-controller-types.ts | 11 +++ .../remote-feature-flag-controller.test.ts | 89 +++++++++++++++++++ .../src/remote-feature-flag-controller.ts | 58 +++++++++++- .../src/utils/user-segmentation-utils.test.ts | 86 ++++++++++++++++++ .../src/utils/user-segmentation-utils.ts | 49 ++++++++++ yarn.lock | 1 + 8 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts create mode 100644 packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index a703989c58..042fef6e6a 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] + +### Added + +- Added support for threshold-based feature flag scoping ([#5051](https://github.com/MetaMask/core/pull/5051)) + - Enables percentage-based feature flag distribution across user base + - Uses deterministic random group assignment based on metaMetricsId + ## [1.1.0] ### Added @@ -26,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of the RemoteFeatureFlagController. ([#4931](https://github.com/MetaMask/core/pull/4931)) - This controller manages the retrieval and caching of remote feature flags. It fetches feature flags from a remote API, caches them, and provides methods to access and manage these flags. The controller ensures that feature flags are refreshed based on a specified interval and handles cases where the controller is disabled or the network is unavailable. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.2.0...HEAD +[1.2.0]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.1.0...@metamask/remote-feature-flag-controller@1.2.0 [1.1.0]: https://github.com/MetaMask/core/compare/@metamask/remote-feature-flag-controller@1.0.0...@metamask/remote-feature-flag-controller@1.1.0 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/remote-feature-flag-controller@1.0.0 diff --git a/packages/remote-feature-flag-controller/package.json b/packages/remote-feature-flag-controller/package.json index 3068aad3d7..9c1d2be9bc 100644 --- a/packages/remote-feature-flag-controller/package.json +++ b/packages/remote-feature-flag-controller/package.json @@ -49,7 +49,8 @@ "dependencies": { "@metamask/base-controller": "^7.0.2", "@metamask/utils": "^10.0.0", - "cockatiel": "^3.1.2" + "cockatiel": "^3.1.2", + "uuid": "^8.3.2" }, "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts index 04cbb71bbb..6e94969e8b 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts @@ -23,6 +23,17 @@ export type FeatureFlags = { [key: string]: Json; }; +export type FeatureFlagScope = { + type: string; + value: number; +}; + +export type FeatureFlagScopeValue = { + name: string; + scope: FeatureFlagScope; + value: Json; +}; + export type ApiDataResponse = FeatureFlags[]; export type ServiceResponse = { diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts index db57256c1b..84e1e224a4 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts @@ -5,6 +5,7 @@ import { RemoteFeatureFlagController, controllerName, DEFAULT_CACHE_DURATION, + getDefaultRemoteFeatureFlagControllerState, } from './remote-feature-flag-controller'; import type { RemoteFeatureFlagControllerActions, @@ -13,14 +14,36 @@ import type { RemoteFeatureFlagControllerStateChangeEvent, } from './remote-feature-flag-controller'; import type { FeatureFlags } from './remote-feature-flag-controller-types'; +import * as userSegmentationUtils from './utils/user-segmentation-utils'; const MOCK_FLAGS: FeatureFlags = { feature1: true, feature2: { chrome: '<109' }, + feature3: [1, 2, 3], }; const MOCK_FLAGS_TWO = { different: true }; +const MOCK_FLAGS_WITH_THRESHOLD = { + ...MOCK_FLAGS, + testFlagForThreshold: [ + { + name: 'groupA', + scope: { type: 'threshold', value: 0.3 }, + value: 'valueA', + }, + { + name: 'groupB', + scope: { type: 'threshold', value: 0.5 }, + value: 'valueB', + }, + { name: 'groupC', scope: { type: 'threshold', value: 1 }, value: 'valueC' }, + ], +}; + +const MOCK_METRICS_ID = 'f9e8d7c6-b5a4-4210-9876-543210fedcba'; +const MOCK_METRICS_ID_2 = '987fcdeb-51a2-4c4b-9876-543210fedcba'; + /** * Creates a controller instance with default parameters for testing * @param options - The controller configuration options @@ -36,6 +59,7 @@ function createController( state: Partial; clientConfigApiService: AbstractClientConfigApiService; disabled: boolean; + getMetaMetricsId: Promise; }> = {}, ) { return new RemoteFeatureFlagController({ @@ -44,6 +68,7 @@ function createController( clientConfigApiService: options.clientConfigApiService ?? buildClientConfigApiService(), disabled: options.disabled, + getMetaMetricsId: options.getMetaMetricsId, }); } @@ -239,6 +264,61 @@ describe('RemoteFeatureFlagController', () => { }); }); + describe('threshold feature flags', () => { + it('processes threshold feature flags based on provided metaMetricsId', async () => { + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: MOCK_FLAGS_WITH_THRESHOLD, + }); + const controller = createController({ + clientConfigApiService, + getMetaMetricsId: Promise.resolve(MOCK_METRICS_ID), + }); + await controller.updateRemoteFeatureFlags(); + + expect( + controller.state.remoteFeatureFlags.testFlagForThreshold, + ).toStrictEqual({ + name: 'groupC', + value: 'valueC', + }); + }); + + it('preserves non-threshold feature flags unchanged', async () => { + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: MOCK_FLAGS_WITH_THRESHOLD, + }); + const controller = createController({ + clientConfigApiService, + getMetaMetricsId: Promise.resolve(MOCK_METRICS_ID), + }); + await controller.updateRemoteFeatureFlags(); + + const { testFlagForThreshold, ...nonThresholdFlags } = + controller.state.remoteFeatureFlags; + expect(nonThresholdFlags).toStrictEqual(MOCK_FLAGS); + }); + + it('uses a fallback metaMetricsId if none is provided', async () => { + jest + .spyOn(userSegmentationUtils, 'generateFallbackMetaMetricsId') + .mockReturnValue(MOCK_METRICS_ID_2); + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: MOCK_FLAGS_WITH_THRESHOLD, + }); + const controller = createController({ + clientConfigApiService, + }); + await controller.updateRemoteFeatureFlags(); + + expect( + controller.state.remoteFeatureFlags.testFlagForThreshold, + ).toStrictEqual({ + name: 'groupA', + value: 'valueA', + }); + }); + }); + describe('enable and disable', () => { it('enables the controller and makes a network request to fetch', async () => { const clientConfigApiService = buildClientConfigApiService(); @@ -273,6 +353,15 @@ describe('RemoteFeatureFlagController', () => { expect(controller.state.remoteFeatureFlags).toStrictEqual(MOCK_FLAGS); }); }); + + describe('getDefaultRemoteFeatureFlagControllerState', () => { + it('should return default state', () => { + expect(getDefaultRemoteFeatureFlagControllerState()).toStrictEqual({ + remoteFeatureFlags: {}, + cacheTimestamp: 0, + }); + }); + }); }); type RootAction = RemoteFeatureFlagControllerActions; diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts index 5fc4161c4d..81f78f5afa 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts @@ -9,7 +9,13 @@ import type { AbstractClientConfigApiService } from './client-config-api-service import type { FeatureFlags, ServiceResponse, + FeatureFlagScopeValue, } from './remote-feature-flag-controller-types'; +import { + generateDeterministicRandomNumber, + isFeatureFlagWithScopeValue, + generateFallbackMetaMetricsId, +} from './utils/user-segmentation-utils'; // === GENERAL === @@ -97,6 +103,8 @@ export class RemoteFeatureFlagController extends BaseController< #inProgressFlagUpdate?: Promise; + #getMetaMetricsId?: Promise; + /** * Constructs a new RemoteFeatureFlagController instance. * @@ -106,6 +114,7 @@ export class RemoteFeatureFlagController extends BaseController< * @param options.clientConfigApiService - The service instance to fetch remote feature flags. * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day. * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false. + * @param options.getMetaMetricsId - Promise that resolves to a metaMetricsId. */ constructor({ messenger, @@ -113,10 +122,12 @@ export class RemoteFeatureFlagController extends BaseController< clientConfigApiService, fetchInterval = DEFAULT_CACHE_DURATION, disabled = false, + getMetaMetricsId, }: { messenger: RemoteFeatureFlagControllerMessenger; state?: Partial; clientConfigApiService: AbstractClientConfigApiService; + getMetaMetricsId?: Promise; fetchInterval?: number; disabled?: boolean; }) { @@ -133,6 +144,7 @@ export class RemoteFeatureFlagController extends BaseController< this.#fetchInterval = fetchInterval; this.#disabled = disabled; this.#clientConfigApiService = clientConfigApiService; + this.#getMetaMetricsId = getMetaMetricsId; } /** @@ -172,7 +184,7 @@ export class RemoteFeatureFlagController extends BaseController< this.#inProgressFlagUpdate = undefined; } - this.#updateCache(serverData.remoteFeatureFlags); + await this.#updateCache(serverData.remoteFeatureFlags); } /** @@ -181,15 +193,55 @@ export class RemoteFeatureFlagController extends BaseController< * @param remoteFeatureFlags - The new feature flags to cache. * @private */ - #updateCache(remoteFeatureFlags: FeatureFlags) { + async #updateCache(remoteFeatureFlags: FeatureFlags) { + const processedRemoteFeatureFlags = await this.#processRemoteFeatureFlags( + remoteFeatureFlags, + ); this.update(() => { return { - remoteFeatureFlags, + remoteFeatureFlags: processedRemoteFeatureFlags, cacheTimestamp: Date.now(), }; }); } + async #processRemoteFeatureFlags( + remoteFeatureFlags: FeatureFlags, + ): Promise { + const processedRemoteFeatureFlags: FeatureFlags = {}; + const metaMetricsId = + (await this.#getMetaMetricsId) || generateFallbackMetaMetricsId(); + const thresholdValue = generateDeterministicRandomNumber(metaMetricsId); + + for (const [ + remoteFeatureFlagName, + remoteFeatureFlagValue, + ] of Object.entries(remoteFeatureFlags)) { + let processedValue = remoteFeatureFlagValue; + + if (Array.isArray(remoteFeatureFlagValue) && thresholdValue) { + const selectedGroup = remoteFeatureFlagValue.find( + (featureFlag): featureFlag is FeatureFlagScopeValue => { + if (!isFeatureFlagWithScopeValue(featureFlag)) { + return false; + } + + return thresholdValue <= featureFlag.scope.value; + }, + ); + if (selectedGroup) { + processedValue = { + name: selectedGroup.name, + value: selectedGroup.value, + }; + } + } + + processedRemoteFeatureFlags[remoteFeatureFlagName] = processedValue; + } + return processedRemoteFeatureFlags; + } + /** * Enables the controller, allowing it to make network requests. */ diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts new file mode 100644 index 0000000000..be5443cbd6 --- /dev/null +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts @@ -0,0 +1,86 @@ +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; + +import { + generateDeterministicRandomNumber, + isFeatureFlagWithScopeValue, + generateFallbackMetaMetricsId, +} from './user-segmentation-utils'; + +const MOCK_METRICS_IDS = [ + '123e4567-e89b-4456-a456-426614174000', + '987fcdeb-51a2-4c4b-9876-543210fedcba', + 'a1b2c3d4-e5f6-4890-abcd-ef1234567890', + 'f9e8d7c6-b5a4-4210-9876-543210fedcba', +]; + +const MOCK_FEATURE_FLAGS = { + VALID: { + name: 'test-flag', + value: true, + scope: { + type: 'threshold', + value: 0.5, + }, + }, + INVALID_NO_SCOPE: { + name: 'test-flag', + value: true, + }, + INVALID_VALUES: ['string', 123, true, null, []], +}; + +describe('user-segmentation-utils', () => { + describe('generateDeterministicRandomNumber', () => { + it('generates consistent numbers for the same input', () => { + const result1 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); + const result2 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); + + expect(result1).toBe(result2); + }); + + it('generates numbers between 0 and 1', () => { + MOCK_METRICS_IDS.forEach((id) => { + const result = generateDeterministicRandomNumber(id); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(1); + }); + }); + + it('generates different numbers for different inputs', () => { + const result1 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[0]); + const result2 = generateDeterministicRandomNumber(MOCK_METRICS_IDS[1]); + + expect(result1).not.toBe(result2); + }); + }); + + describe('isFeatureFlagWithScopeValue', () => { + it('returns true for valid feature flag with scope', () => { + expect(isFeatureFlagWithScopeValue(MOCK_FEATURE_FLAGS.VALID)).toBe(true); + }); + + it('returns false for null', () => { + expect(isFeatureFlagWithScopeValue(null)).toBe(false); + }); + + it('returns false for non-objects', () => { + MOCK_FEATURE_FLAGS.INVALID_VALUES.forEach((value) => { + expect(isFeatureFlagWithScopeValue(value)).toBe(false); + }); + }); + + it('returns false for objects without scope', () => { + expect( + isFeatureFlagWithScopeValue(MOCK_FEATURE_FLAGS.INVALID_NO_SCOPE), + ).toBe(false); + }); + }); + + describe('generateFallbackMetaMetricsId', () => { + it('returns a valid uuidv4', () => { + const result = generateFallbackMetaMetricsId(); + expect(uuidValidate(result)).toBe(true); + expect(uuidVersion(result)).toBe(4); + }); + }); +}); diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts new file mode 100644 index 0000000000..811f09e106 --- /dev/null +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts @@ -0,0 +1,49 @@ +import type { Json } from '@metamask/utils'; +import { v4 as uuidV4 } from 'uuid'; + +import type { FeatureFlagScopeValue } from '../remote-feature-flag-controller-types'; + +/* eslint-disable no-bitwise */ +/** + * Generates a deterministic random number between 0 and 1 based on a metaMetricsId. + * This is useful for A/B testing and feature flag rollouts where we want + * consistent group assignment for the same user. + * + * @param metaMetricsId - The unique identifier used to generate the deterministic random number + * @returns A number between 0 and 1 that is deterministic for the given metaMetricsId + */ +export function generateDeterministicRandomNumber( + metaMetricsId: string, +): number { + const hash = [...metaMetricsId].reduce((acc, char) => { + const chr = char.charCodeAt(0); + return ((acc << 5) - acc + chr) | 0; + }, 0); + + return (hash >>> 0) / 0xffffffff; +} + +/** + * Type guard to check if a value is a feature flag with scope. + * Used to validate feature flag objects that contain scope-based configurations. + * + * @param featureFlag - The value to check if it's a feature flag with scope + * @returns True if the value is a feature flag with scope, false otherwise + */ +export const isFeatureFlagWithScopeValue = ( + featureFlag: Json, +): featureFlag is FeatureFlagScopeValue => { + return ( + typeof featureFlag === 'object' && + featureFlag !== null && + 'scope' in featureFlag + ); +}; + +/** + * Generates UUIDv4 as a fallback metaMetricsId + * @returns A UUIDv4 string + */ +export function generateFallbackMetaMetricsId(): string { + return uuidV4(); +} diff --git a/yarn.lock b/yarn.lock index cbc8b3365e..60e7d188b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3521,6 +3521,7 @@ __metadata: typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" + uuid: "npm:^8.3.2" languageName: unknown linkType: soft From ed8677eb879cf923dd2d5c61805ff980c204a674 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:00:26 -0800 Subject: [PATCH 15/23] refactor(permission-controller): Rationalize permission and caveat validation (#5062) ## Explanation Via discussion during https://github.com/MetaMask/metamask-extension/pull/27847, it became apparent that there was confusion re: the purpose of permission vs. caveat validators and when to use one vs. the other. This has now been properly documented in multiple places in the permission controller package. In addition, the permission controller implementation contained some irregularities regarding its own permission validation logic. This surfaced in the form of redundant permission and caveat validator calls, both of which have now been removed. (In addition, one overly complicated "optimization" to avoid redundant caveat validator calls has been removed.) We have verified that permission validators aren't used for caveat validation anywhere in our codebase. ## References - https://github.com/MetaMask/metamask-extension/pull/27847#discussion_r1879060900 ## Changelog ### `@metamask/permission-controller` - **CHANGED**: Remove redundant caveat validator calls - In some cases, caveats were being validated multiple times or without the possibility of having changed. - The intended purpose of permission and caveat validators has also been documented. See `ARCHITECTURE.md`. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../permission-controller/ARCHITECTURE.md | 53 +++++++++++++++++++ packages/permission-controller/src/Caveat.ts | 9 ++-- .../permission-controller/src/Permission.ts | 8 ++- .../src/PermissionController.ts | 47 ++++++++++------ 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/packages/permission-controller/ARCHITECTURE.md b/packages/permission-controller/ARCHITECTURE.md index 8c715234ba..bce0a40177 100644 --- a/packages/permission-controller/ARCHITECTURE.md +++ b/packages/permission-controller/ARCHITECTURE.md @@ -165,6 +165,38 @@ C = { foo: 'baz', life: 42 }; Delta = { foo: 'baz' }; ``` +### Specifying permissions and caveats + +Permissions and caveats are specified by constructing _specification objects_, +which are passed to the `PermissionController` constructor. See the [construction examples](#construction) +for how to do this. + +#### Permission and caveat validators + +Permission and caveat specifications optionally include a `validator` function. +This function is called to validate the permission or caveat when they change. +If validation fails, the validator function should throw an appropriate JSON-RPC error. + +The validators are invoked in the following cases: + +- Permission validators + - When a permission is granted + - When a permission's caveat array is mutated +- Caveat validators + - When a caveat is constructed + - When a caveat's value is mutated + +Notice that permission validators are only invoked when a permission's caveat array is mutated, +not when an individual caveat is mutated. This means that permission validators **must not** +be relied upon to validate caveat values. + +This establishes a separation of concerns between permission validators and caveat validators. +In brief: + +- Caveat validators are inherently unaware of the permissions that they are caveats of. +- Permission validators **should** be unaware of the internal structure of their caveats. + - However, they **may** be used to verify the membership of its caveat array. + ### Requesting permissions The `PermissionController` provides two methods for requesting permissions: @@ -221,6 +253,15 @@ const caveatSpecifications = { caveat.value.includes(resultValue), ); }, + validator: (caveat: { type: 'filterArrayResponse'; value: Json }) => { + // This function is called to validate the value of a caveat. + // If the value is invalid, the request will fail. By way of example, + // we could check that the value is an array of strings: + return ( + Array.isArray(caveat.value) && + caveat.value.every((v) => typeof v === 'string') + ); + }, // This function is called if two caveats of this type have to be merged // due to an incremental permissions request. The values must be merged // in the fashion of a right-biased union. @@ -238,6 +279,18 @@ const permissionSpecifications = { // i.e. the restricted method name targetName: 'wallet_getSecretArray', allowedCaveats: ['filterArrayResponse'], + validator: (permission: PermissionConstraint) => { + // This function is called to validate the permission. + // If the permission is invalid, the request will fail. + // By way of example, we could check that the permission has at least + // one caveat of type 'filterArrayResponse'. + assert.ok( + permission.caveats?.some( + (caveat) => caveat.type === CaveatTypes.filterArrayResponse, + ), + 'getSecretArray permission validation failed', + ); + }, // Every restricted method must specify its implementation in its // specification. methodImplementation: ( diff --git a/packages/permission-controller/src/Caveat.ts b/packages/permission-controller/src/Caveat.ts index 7bfb57b199..63f455560b 100644 --- a/packages/permission-controller/src/Caveat.ts +++ b/packages/permission-controller/src/Caveat.ts @@ -137,14 +137,15 @@ export type CaveatSpecificationBase = { /** * The validator function used to validate caveats of the associated type - * whenever they are instantiated. Caveat are instantiated whenever they are - * created or mutated. + * whenever they are constructed or mutated. * * The validator should throw an appropriate JSON-RPC error if validation fails. * * If no validator is specified, no validation of caveat values will be - * performed. Although caveats can also be validated by permission validators, - * validating caveat values separately is strongly recommended. + * performed. In instances where caveats are mutated but a permission's caveat + * array has not changed, any corresponding permission validator will not be + * called. For this reason, permission validators **must not** be relied upon + * to validate caveats. */ // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/permission-controller/src/Permission.ts b/packages/permission-controller/src/Permission.ts index d5aa7e7b8c..809cc168dd 100644 --- a/packages/permission-controller/src/Permission.ts +++ b/packages/permission-controller/src/Permission.ts @@ -405,8 +405,12 @@ type PermissionSpecificationBase = { /** * The validator function used to validate permissions of the associated type - * whenever they are mutated. The only way a permission can be legally mutated - * is when its caveats are modified by the permission controller. + * whenever they are granted or their caveat arrays are mutated. + * + * Permission validators are **not** invoked when a caveat is mutated, provided + * the caveat array has not changed. For this reason, permission validators + * **must not** be used to validate caveats. To validate caveats, use the + * corresponding caveat specification property. * * The validator should throw an appropriate JSON-RPC error if validation fails. */ diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 359566f43a..b5e33393f8 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -95,6 +95,14 @@ import { getPermissionMiddlewareFactory } from './permission-middleware'; import type { GetSubjectMetadata } from './SubjectMetadataController'; import { collectUniqueAndPairedCaveats, MethodNames } from './utils'; +/** + * Flags for controlling the validation behavior of certain internal methods. + */ +type PermissionValidationFlags = { + invokePermissionValidator: boolean; + performCaveatValidation: boolean; +}; + /** * Metadata associated with {@link PermissionController} subjects. */ @@ -1363,6 +1371,7 @@ export class PermissionController< }; this.validateCaveat(caveat, origin, target); + let addedCaveat = false; if (permission.caveats) { const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveat.type, @@ -1370,6 +1379,7 @@ export class PermissionController< if (caveatIndex === -1) { permission.caveats.push(caveat); + addedCaveat = true; } else { permission.caveats.splice(caveatIndex, 1, caveat); } @@ -1380,9 +1390,17 @@ export class PermissionController< // the permission validator is also called. // @ts-expect-error See above comment permission.caveats = [caveat]; + addedCaveat = true; } - this.validateModifiedPermission(permission, origin); + // Mutating a caveat does not warrant permission validation, but mutating + // the caveat array does. + if (addedCaveat) { + this.validateModifiedPermission(permission, origin, { + invokePermissionValidator: true, + performCaveatValidation: false, // We just validated the caveat + }); + } }); } @@ -1556,12 +1574,15 @@ export class PermissionController< permission.caveats.splice(caveatIndex, 1); } - this.validateModifiedPermission(permission, origin); + this.validateModifiedPermission(permission, origin, { + invokePermissionValidator: true, + performCaveatValidation: false, // No caveat object was mutated + }); } /** * Validates the specified modified permission. Should **always** be invoked - * on a permission after its caveats have been modified. + * on a permission when its caveat array has been mutated. * * Just like {@link PermissionController.validatePermission}, except that the * corresponding target name and specification are retrieved first, and an @@ -1569,10 +1590,12 @@ export class PermissionController< * * @param permission - The modified permission to validate. * @param origin - The origin associated with the permission. + * @param validationFlags - Validation flags. See {@link PermissionController.validatePermission}. */ private validateModifiedPermission( permission: Draft, origin: OriginString, + validationFlags: PermissionValidationFlags, ): void { /* istanbul ignore if: this should be impossible */ if (!this.targetExists(permission.parentCapability)) { @@ -1585,6 +1608,7 @@ export class PermissionController< this.getPermissionSpecification(permission.parentCapability), permission as PermissionConstraint, origin, + validationFlags, ); } @@ -1773,17 +1797,10 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >; - let performCaveatValidation = true; - if (specification.factory) { permission = specification.factory(permissionOptions, requestData); } else { permission = constructPermission(permissionOptions); - - // We do not need to validate caveats in this case, because the plain - // permission constructor function does not modify the caveats, which - // were already validated by `constructCaveats` above. - performCaveatValidation = false; } if (mergePermissions) { @@ -1795,7 +1812,7 @@ export class PermissionController< this.validatePermission(specification, permission, origin, { invokePermissionValidator: true, - performCaveatValidation, + performCaveatValidation: true, }); permissions[targetName] = permission; } @@ -1829,10 +1846,10 @@ export class PermissionController< specification: PermissionSpecificationConstraint, permission: PermissionConstraint, origin: OriginString, - { invokePermissionValidator, performCaveatValidation } = { - invokePermissionValidator: true, - performCaveatValidation: true, - }, + { + invokePermissionValidator, + performCaveatValidation, + }: PermissionValidationFlags, ): void { const { allowedCaveats, validator, targetName } = specification; From bf934589ee01ea5f3c379e52127afd7eb615ea7e Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Thu, 12 Dec 2024 18:42:07 +0000 Subject: [PATCH 16/23] Release 271.0.0 (#5063) ## Explanation Added support for threshold-based feature flag scoping in `remote-feature-flag-controller` ## References N/A ## Changelog N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes Co-authored-by: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> --- package.json | 2 +- packages/remote-feature-flag-controller/CHANGELOG.md | 2 +- packages/remote-feature-flag-controller/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 52d0b51f10..e1b7daf616 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "270.0.0", + "version": "271.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index 042fef6e6a..c4447ac44c 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for threshold-based feature flag scoping ([#5051](https://github.com/MetaMask/core/pull/5051)) - Enables percentage-based feature flag distribution across user base - - Uses deterministic random group assignment based on metaMetricsId + - Uses deterministic random group assignment based on metaMetricsId from the client ## [1.1.0] diff --git a/packages/remote-feature-flag-controller/package.json b/packages/remote-feature-flag-controller/package.json index 9c1d2be9bc..6ece55ba38 100644 --- a/packages/remote-feature-flag-controller/package.json +++ b/packages/remote-feature-flag-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/remote-feature-flag-controller", - "version": "1.1.0", + "version": "1.2.0", "description": "The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags", "keywords": [ "MetaMask", From 3f15e1ed1f26ced9f021b3c08a3f26feec7d6c18 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 12 Dec 2024 16:24:14 -0600 Subject: [PATCH 17/23] Revert "chore: Remove `useRequestQueue` toggle to turn on/off Per Dapp Selected Network Feature (#4941)" (#5065) This reverts commit 4814cf11670c76f7d3015206dbe3c78b383ddc3a. ## Explanation We are not yet ready to release per-dapp selected network functionality on the mobile client and with this change there is no clean way to [update the version of SelectedNetworkController](https://github.com/MetaMask/metamask-mobile/issues/12434#issuecomment-2537358920) in the mobile client without having the Domains state starting to populate and possibly become corrupt since its not being consumed by/updated by the frontend in the expected way and may need to be migrated away when its time to actually start using the controller. Without this revert the @MetaMask/wallet-framework team is blocked from completing their goal to get both clients up to the latest versions of all controllers. ## Changelog ### `@metamask/queued-request-controller` - **ADDED**: **BREAKING:** `createQueuedRequestMiddleware` now expects a `useRequestQueue` property in its param options ### `@metamask/selected-network-controller` - **ADDED**: **BREAKING:** `SelectedNetworkController` constructor now expects both a `useRequestQueuePreference` and a `onPreferencesStateChange` param ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../src/QueuedRequestMiddleware.test.ts | 9 +- .../src/QueuedRequestMiddleware.ts | 8 +- .../src/SelectedNetworkController.ts | 62 +- .../tests/SelectedNetworkController.test.ts | 1061 +++++++++++------ 4 files changed, 746 insertions(+), 394 deletions(-) diff --git a/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts b/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts index 80738262d3..6af151aae0 100644 --- a/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts +++ b/packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts @@ -86,6 +86,7 @@ describe('createQueuedRequestMiddleware', () => { const mockEnqueueRequest = getMockEnqueueRequest(); const middleware = buildQueuedRequestMiddleware({ enqueueRequest: mockEnqueueRequest, + useRequestQueue: () => true, }); const request = { @@ -104,7 +105,7 @@ describe('createQueuedRequestMiddleware', () => { const mockEnqueueRequest = getMockEnqueueRequest(); const middleware = buildQueuedRequestMiddleware({ enqueueRequest: mockEnqueueRequest, - + useRequestQueue: () => true, shouldEnqueueRequest: ({ method }) => method === 'method_with_confirmation', }); @@ -144,6 +145,7 @@ describe('createQueuedRequestMiddleware', () => { it('calls next after a request is queued and processed', async () => { const middleware = buildQueuedRequestMiddleware({ enqueueRequest: getMockEnqueueRequest(), + useRequestQueue: () => true, }); const request = { ...getRequestDefaults(), @@ -165,7 +167,7 @@ describe('createQueuedRequestMiddleware', () => { enqueueRequest: jest .fn() .mockRejectedValue(new Error('enqueuing error')), - + useRequestQueue: () => true, shouldEnqueueRequest: () => true, }); const request = { @@ -189,7 +191,7 @@ describe('createQueuedRequestMiddleware', () => { enqueueRequest: jest .fn() .mockRejectedValue(new Error('enqueuing error')), - + useRequestQueue: () => true, shouldEnqueueRequest: () => true, }); const request = { @@ -269,6 +271,7 @@ function buildQueuedRequestMiddleware( ) { const options = { enqueueRequest: getMockEnqueueRequest(), + useRequestQueue: () => false, shouldEnqueueRequest: () => false, ...overrideOptions, }; diff --git a/packages/queued-request-controller/src/QueuedRequestMiddleware.ts b/packages/queued-request-controller/src/QueuedRequestMiddleware.ts index 5b52fb649b..5edecf787e 100644 --- a/packages/queued-request-controller/src/QueuedRequestMiddleware.ts +++ b/packages/queued-request-controller/src/QueuedRequestMiddleware.ts @@ -38,14 +38,17 @@ function hasRequiredMetadata( * * @param options - Configuration options. * @param options.enqueueRequest - A method for enqueueing a request. + * @param options.useRequestQueue - A function that determines if the request queue feature is enabled. * @param options.shouldEnqueueRequest - A function that returns if a request should be handled by the QueuedRequestController. * @returns The JSON-RPC middleware that manages queued requests. */ export const createQueuedRequestMiddleware = ({ enqueueRequest, + useRequestQueue, shouldEnqueueRequest, }: { enqueueRequest: QueuedRequestController['enqueueRequest']; + useRequestQueue: () => boolean; shouldEnqueueRequest: ( request: QueuedRequestMiddlewareJsonRpcRequest, ) => boolean; @@ -53,8 +56,9 @@ export const createQueuedRequestMiddleware = ({ return createAsyncMiddleware(async (req: JsonRpcRequest, res, next) => { hasRequiredMetadata(req); - // if this method is not a confirmation method bypass the queue completely - if (!shouldEnqueueRequest(req)) { + // if the request queue feature is turned off, or this method is not a confirmation method + // bypass the queue completely + if (!useRequestQueue() || !shouldEnqueueRequest(req)) { return await next(); } diff --git a/packages/selected-network-controller/src/SelectedNetworkController.ts b/packages/selected-network-controller/src/SelectedNetworkController.ts index ab87c541ca..8f73418122 100644 --- a/packages/selected-network-controller/src/SelectedNetworkController.ts +++ b/packages/selected-network-controller/src/SelectedNetworkController.ts @@ -102,6 +102,10 @@ export type SelectedNetworkControllerMessenger = RestrictedControllerMessenger< export type SelectedNetworkControllerOptions = { state?: SelectedNetworkControllerState; messenger: SelectedNetworkControllerMessenger; + useRequestQueuePreference: boolean; + onPreferencesStateChange: ( + listener: (preferencesState: { useRequestQueue: boolean }) => void, + ) => void; domainProxyMap: Map; }; @@ -120,17 +124,23 @@ export class SelectedNetworkController extends BaseController< > { #domainProxyMap: Map; + #useRequestQueuePreference: boolean; + /** * Construct a SelectedNetworkController controller. * * @param options - The controller options. * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. * @param options.state - The controllers initial state. + * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference. + * @param options.onPreferencesStateChange - A callback that is called when the preference state changes. * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use. */ constructor({ messenger, state = getDefaultState(), + useRequestQueuePreference, + onPreferencesStateChange, domainProxyMap, }: SelectedNetworkControllerOptions) { super({ @@ -139,6 +149,7 @@ export class SelectedNetworkController extends BaseController< messenger, state, }); + this.#useRequestQueuePreference = useRequestQueuePreference; this.#domainProxyMap = domainProxyMap; this.#registerMessageHandlers(); @@ -236,6 +247,21 @@ export class SelectedNetworkController extends BaseController< } }, ); + + onPreferencesStateChange(({ useRequestQueue }) => { + if (this.#useRequestQueuePreference !== useRequestQueue) { + if (!useRequestQueue) { + // Loop through all domains and points each domain's proxy + // to the NetworkController's own proxy of the globally selected networkClient + Object.keys(this.state.domains).forEach((domain) => { + this.#unsetNetworkClientIdForDomain(domain); + }); + } else { + this.#resetAllPermissionedDomains(); + } + this.#useRequestQueuePreference = useRequestQueue; + } + }); } #registerMessageHandlers(): void { @@ -300,10 +326,31 @@ export class SelectedNetworkController extends BaseController< ); } + // Loop through all domains and for those with permissions it points that domain's proxy + // to an unproxied instance of the globally selected network client. + // NOT the NetworkController's proxy of the globally selected networkClient + #resetAllPermissionedDomains() { + this.#domainProxyMap.forEach((_: NetworkProxy, domain: string) => { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + // can't use public setNetworkClientIdForDomain because it will throw an error + // rather than simply skip if the domain doesn't have permissions which can happen + // in this case since proxies are added for each site the user visits + if (this.#domainHasPermissions(domain)) { + this.#setNetworkClientIdForDomain(domain, selectedNetworkClientId); + } + }); + } + setNetworkClientIdForDomain( domain: Domain, networkClientId: NetworkClientId, ) { + if (!this.#useRequestQueuePreference) { + return; + } + if (domain === METAMASK_DOMAIN) { throw new Error( `NetworkClientId for domain "${METAMASK_DOMAIN}" cannot be set on the SelectedNetworkController`, @@ -326,6 +373,9 @@ export class SelectedNetworkController extends BaseController< getNetworkClientIdForDomain(domain: Domain): NetworkClientId { const { selectedNetworkClientId: metamaskSelectedNetworkClientId } = this.messagingSystem.call('NetworkController:getState'); + if (!this.#useRequestQueuePreference) { + return metamaskSelectedNetworkClientId; + } return this.state.domains[domain] ?? metamaskSelectedNetworkClientId; } @@ -353,7 +403,10 @@ export class SelectedNetworkController extends BaseController< let networkProxy = this.#domainProxyMap.get(domain); if (networkProxy === undefined) { let networkClient; - if (this.#domainHasPermissions(domain)) { + if ( + this.#useRequestQueuePreference && + this.#domainHasPermissions(domain) + ) { const networkClientId = this.getNetworkClientIdForDomain(domain); networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', @@ -363,11 +416,10 @@ export class SelectedNetworkController extends BaseController< networkClient = this.messagingSystem.call( 'NetworkController:getSelectedNetworkClient', ); + if (networkClient === undefined) { + throw new Error('Selected network not initialized'); + } } - if (networkClient === undefined) { - throw new Error('Selected network not initialized'); - } - networkProxy = { provider: createEventEmitterProxy(networkClient.provider), blockTracker: createEventEmitterProxy(networkClient.blockTracker, { diff --git a/packages/selected-network-controller/tests/SelectedNetworkController.test.ts b/packages/selected-network-controller/tests/SelectedNetworkController.test.ts index dcc01ae78d..fd975016ca 100644 --- a/packages/selected-network-controller/tests/SelectedNetworkController.test.ts +++ b/packages/selected-network-controller/tests/SelectedNetworkController.test.ts @@ -121,10 +121,15 @@ jest.mock('@metamask/swappable-obj-proxy'); const setup = ({ getSubjectNames = [], state, + useRequestQueuePreference = false, domainProxyMap = new Map(), }: { state?: SelectedNetworkControllerState; getSubjectNames?: string[]; + useRequestQueuePreference?: boolean; + onPreferencesStateChange?: ( + listener: (preferencesState: { useRequestQueue: boolean }) => void, + ) => void; domainProxyMap?: Map; } = {}) => { const mockProviderProxy = { @@ -168,18 +173,34 @@ const setup = ({ getSubjectNames, }); + const preferencesStateChangeListeners: ((state: { + useRequestQueue: boolean; + }) => void)[] = []; const controller = new SelectedNetworkController({ messenger: restrictedMessenger, state, + useRequestQueuePreference, + onPreferencesStateChange: (listener) => { + preferencesStateChangeListeners.push(listener); + }, domainProxyMap, }); + const triggerPreferencesStateChange = (preferencesState: { + useRequestQueue: boolean; + }) => { + for (const listener of preferencesStateChangeListeners) { + listener(preferencesState); + } + }; + return { controller, messenger, mockProviderProxy, mockBlockTrackerProxy, domainProxyMap, + triggerPreferencesStateChange, createEventEmitterProxyMock, ...mockMessengerActions, }; @@ -205,232 +226,296 @@ describe('SelectedNetworkController', () => { }); }); - it('should set networkClientId for domains not already in state', async () => { - const { controller } = setup({ - state: { - domains: { - 'existingdomain.com': 'initialNetworkId', + describe('when useRequestQueuePreference is true', () => { + it('should set networkClientId for domains not already in state', async () => { + const { controller } = setup({ + state: { + domains: { + 'existingdomain.com': 'initialNetworkId', + }, }, - }, - getSubjectNames: ['newdomain.com'], + getSubjectNames: ['newdomain.com'], + useRequestQueuePreference: true, + }); + + expect(controller.state.domains).toStrictEqual({ + 'newdomain.com': 'mainnet', + 'existingdomain.com': 'initialNetworkId', + }); }); - expect(controller.state.domains).toStrictEqual({ - 'newdomain.com': 'mainnet', - 'existingdomain.com': 'initialNetworkId', + it('should not modify domains already in state', async () => { + const { controller } = setup({ + state: { + domains: { + 'existingdomain.com': 'initialNetworkId', + }, + }, + getSubjectNames: ['existingdomain.com'], + useRequestQueuePreference: true, + }); + + expect(controller.state.domains).toStrictEqual({ + 'existingdomain.com': 'initialNetworkId', + }); }); }); - it('should not modify domains already in state', async () => { - const { controller } = setup({ - state: { - domains: { - 'existingdomain.com': 'initialNetworkId', + describe('when useRequestQueuePreference is false', () => { + it('should not set networkClientId for new domains', async () => { + const { controller } = setup({ + state: { + domains: { + 'existingdomain.com': 'initialNetworkId', + }, }, - }, - getSubjectNames: ['existingdomain.com'], + getSubjectNames: ['newdomain.com'], + }); + + expect(controller.state.domains).toStrictEqual({ + 'existingdomain.com': 'initialNetworkId', + }); }); - expect(controller.state.domains).toStrictEqual({ - 'existingdomain.com': 'initialNetworkId', + it('should not modify domains already in state', async () => { + const { controller } = setup({ + state: { + domains: { + 'existingdomain.com': 'initialNetworkId', + }, + }, + getSubjectNames: ['existingdomain.com'], + }); + + expect(controller.state.domains).toStrictEqual({ + 'existingdomain.com': 'initialNetworkId', + }); }); }); + }); - describe('NetworkController:stateChange', () => { - describe('when a network is deleted from the network controller', () => { - const initialDomains = { - 'not-deleted-network.com': 'linea-mainnet', - 'deleted-network.com': 'goerli', - }; + describe('NetworkController:stateChange', () => { + describe('when a network is deleted from the network controller', () => { + const initialDomains = { + 'not-deleted-network.com': 'linea-mainnet', + 'deleted-network.com': 'goerli', + }; + + const deleteNetwork = ( + chainId: Hex, + networkControllerState: NetworkState, + messenger: ReturnType, + mockNetworkControllerGetState: jest.Mock, + ) => { + delete networkControllerState.networkConfigurationsByChainId[chainId]; + mockNetworkControllerGetState.mockReturnValueOnce( + networkControllerState, + ); + messenger.publish( + 'NetworkController:stateChange', + networkControllerState, + [ + { + op: 'remove', + path: ['networkConfigurationsByChainId', chainId], + }, + ], + ); + }; - const deleteNetwork = ( - chainId: Hex, - networkControllerState: NetworkState, - messenger: ReturnType, - mockNetworkControllerGetState: jest.Mock, - ) => { - delete networkControllerState.networkConfigurationsByChainId[chainId]; - mockNetworkControllerGetState.mockReturnValueOnce( - networkControllerState, - ); - messenger.publish( - 'NetworkController:stateChange', - networkControllerState, - [ - { - op: 'remove', - path: ['networkConfigurationsByChainId', chainId], - }, - ], - ); - }; + it('does not update state when useRequestQueuePreference is false', () => { + const { controller, messenger, mockNetworkControllerGetState } = setup({ + state: { domains: initialDomains }, + useRequestQueuePreference: false, + }); - it('redirects domains to the globally selected network', () => { - const { controller, messenger, mockNetworkControllerGetState } = - setup({ - state: { domains: initialDomains }, - }); + const networkControllerState = getDefaultNetworkControllerState(); + deleteNetwork( + '0x5', + networkControllerState, + messenger, + mockNetworkControllerGetState, + ); - const networkControllerState = { - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'mainnet', - }; + expect(controller.state.domains).toStrictEqual(initialDomains); + }); - deleteNetwork( - '0x5', - networkControllerState, - messenger, - mockNetworkControllerGetState, - ); + it('redirects domains to the globally selected network when useRequestQueuePreference is true', () => { + const { controller, messenger, mockNetworkControllerGetState } = setup({ + state: { domains: initialDomains }, + useRequestQueuePreference: true, + }); - expect(controller.state.domains).toStrictEqual({ - ...initialDomains, - 'deleted-network.com': - networkControllerState.selectedNetworkClientId, - }); + const networkControllerState = { + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'mainnet', + }; + + deleteNetwork( + '0x5', + networkControllerState, + messenger, + mockNetworkControllerGetState, + ); + + expect(controller.state.domains).toStrictEqual({ + ...initialDomains, + 'deleted-network.com': networkControllerState.selectedNetworkClientId, }); + }); - it('redirects domains to the globally selected network and handles garbage collected proxies', () => { - const domainProxyMap = new Map(); - const { - controller, - messenger, - mockNetworkControllerGetState, - mockGetNetworkClientById, - } = setup({ - state: { domains: initialDomains }, + it('redirects domains to the globally selected network when useRequestQueuePreference is true and handles garbage collected proxies', () => { + const domainProxyMap = new Map(); + const { + controller, + messenger, + mockNetworkControllerGetState, + mockGetNetworkClientById, + } = setup({ + state: { domains: initialDomains }, + useRequestQueuePreference: true, + domainProxyMap, + }); - domainProxyMap, - }); + // Simulate proxies being garbage collected + domainProxyMap.clear(); + + const networkControllerState = { + ...getDefaultNetworkControllerState(), + selectedNetworkClientId: 'mainnet', + }; - // Simulate proxies being garbage collected - domainProxyMap.clear(); + mockGetNetworkClientById.mockImplementation((id) => { + // Simulate the previous domain being deleted in NetworkController + if (id !== 'mainnet') { + throw new Error('Network client does not exist'); + } - const networkControllerState = { - ...getDefaultNetworkControllerState(), - selectedNetworkClientId: 'mainnet', + return { + provider: { request: jest.fn() }, + blockTracker: { getLatestBlock: jest.fn() }, }; + }); - mockGetNetworkClientById.mockImplementation((id) => { - // Simulate the previous domain being deleted in NetworkController - if (id !== 'mainnet') { - throw new Error('Network client does not exist'); - } + deleteNetwork( + '0x5', + networkControllerState, + messenger, + mockNetworkControllerGetState, + ); - return { - provider: { request: jest.fn() }, - blockTracker: { getLatestBlock: jest.fn() }, - }; - }); + expect(controller.state.domains).toStrictEqual({ + ...initialDomains, + 'deleted-network.com': networkControllerState.selectedNetworkClientId, + }); + }); + }); - deleteNetwork( - '0x5', - networkControllerState, - messenger, - mockNetworkControllerGetState, - ); + describe('when a network is updated', () => { + it('redirects domains when the default rpc endpoint is switched', () => { + const initialDomains = { + 'different-chain.com': 'mainnet', + 'chain-with-new-default.com': 'goerli', + }; - expect(controller.state.domains).toStrictEqual({ - ...initialDomains, - 'deleted-network.com': - networkControllerState.selectedNetworkClientId, - }); + const { controller, messenger, mockNetworkControllerGetState } = setup({ + state: { domains: initialDomains }, + useRequestQueuePreference: true, }); - }); - describe('when a network is updated', () => { - it('redirects domains when the default rpc endpoint is switched', () => { - const initialDomains = { - 'different-chain.com': 'mainnet', - 'chain-with-new-default.com': 'goerli', - }; + const networkControllerState = getDefaultNetworkControllerState(); + const goerliNetwork = + networkControllerState.networkConfigurationsByChainId['0x5']; - const { controller, messenger, mockNetworkControllerGetState } = - setup({ - state: { domains: initialDomains }, - }); + goerliNetwork.defaultRpcEndpointIndex = + goerliNetwork.rpcEndpoints.push({ + type: RpcEndpointType.Custom, + url: 'https://new-default.com', + networkClientId: 'new-default-network-client-id', + }) - 1; - const networkControllerState = getDefaultNetworkControllerState(); - const goerliNetwork = - networkControllerState.networkConfigurationsByChainId['0x5']; + mockNetworkControllerGetState.mockReturnValueOnce( + networkControllerState, + ); - goerliNetwork.defaultRpcEndpointIndex = - goerliNetwork.rpcEndpoints.push({ - type: RpcEndpointType.Custom, - url: 'https://new-default.com', - networkClientId: 'new-default-network-client-id', - }) - 1; + messenger.publish( + 'NetworkController:stateChange', + networkControllerState, + [ + { + op: 'replace', + path: ['networkConfigurationsByChainId', '0x5'], + }, + ], + ); - mockNetworkControllerGetState.mockReturnValueOnce( - networkControllerState, - ); + expect(controller.state.domains).toStrictEqual({ + ...initialDomains, + 'chain-with-new-default.com': 'new-default-network-client-id', + }); + }); - messenger.publish( - 'NetworkController:stateChange', - networkControllerState, - [ - { - op: 'replace', - path: ['networkConfigurationsByChainId', '0x5'], - }, - ], - ); + it('redirects domains when the default rpc endpoint is deleted and replaced', () => { + const initialDomains = { + 'different-chain.com': 'mainnet', + 'chain-with-new-default.com': 'goerli', + }; - expect(controller.state.domains).toStrictEqual({ - ...initialDomains, - 'chain-with-new-default.com': 'new-default-network-client-id', - }); + const { controller, messenger, mockNetworkControllerGetState } = setup({ + state: { domains: initialDomains }, + useRequestQueuePreference: true, }); - it('redirects domains when the default rpc endpoint is deleted and replaced', () => { - const initialDomains = { - 'different-chain.com': 'mainnet', - 'chain-with-new-default.com': 'goerli', - }; + const networkControllerState = getDefaultNetworkControllerState(); + const goerliNetwork = + networkControllerState.networkConfigurationsByChainId['0x5']; - const { controller, messenger, mockNetworkControllerGetState } = - setup({ - state: { domains: initialDomains }, - }); + goerliNetwork.rpcEndpoints = [ + { + type: RpcEndpointType.Custom, + url: 'https://new-default.com', + networkClientId: 'new-default-network-client-id', + }, + ]; - const networkControllerState = getDefaultNetworkControllerState(); - const goerliNetwork = - networkControllerState.networkConfigurationsByChainId['0x5']; + mockNetworkControllerGetState.mockReturnValueOnce( + networkControllerState, + ); - goerliNetwork.rpcEndpoints = [ + messenger.publish( + 'NetworkController:stateChange', + networkControllerState, + [ { - type: RpcEndpointType.Custom, - url: 'https://new-default.com', - networkClientId: 'new-default-network-client-id', + op: 'replace', + path: ['networkConfigurationsByChainId', '0x5'], }, - ]; - - mockNetworkControllerGetState.mockReturnValueOnce( - networkControllerState, - ); - - messenger.publish( - 'NetworkController:stateChange', - networkControllerState, - [ - { - op: 'replace', - path: ['networkConfigurationsByChainId', '0x5'], - }, - ], - ); + ], + ); - expect(controller.state.domains).toStrictEqual({ - ...initialDomains, - 'chain-with-new-default.com': 'new-default-network-client-id', - }); + expect(controller.state.domains).toStrictEqual({ + ...initialDomains, + 'chain-with-new-default.com': 'new-default-network-client-id', }); }); }); + }); + + describe('setNetworkClientIdForDomain', () => { + it('does not update state when the useRequestQueuePreference is false', () => { + const { controller } = setup({ + state: { + domains: {}, + }, + }); + + controller.setNetworkClientIdForDomain('1.com', '1'); + expect(controller.state.domains).toStrictEqual({}); + }); - describe('setNetworkClientIdForDomain', () => { + describe('when useRequestQueuePreference is true', () => { it('should throw an error when passed "metamask" as domain arg', () => { - const { controller } = setup(); + const { controller } = setup({ useRequestQueuePreference: true }); expect(() => { controller.setNetworkClientIdForDomain('metamask', 'mainnet'); }).toThrow( @@ -443,6 +528,7 @@ describe('SelectedNetworkController', () => { it('skips setting the networkClientId for the passed in domain', () => { const { controller, mockHasPermissions } = setup({ state: { domains: {} }, + useRequestQueuePreference: true, }); mockHasPermissions.mockReturnValue(true); const snapDomainOne = 'npm:@metamask/bip32-example-snap'; @@ -473,6 +559,7 @@ describe('SelectedNetworkController', () => { it('sets the networkClientId for the passed in domain', () => { const { controller, mockHasPermissions } = setup({ state: { domains: {} }, + useRequestQueuePreference: true, }); mockHasPermissions.mockReturnValue(true); const domain = 'example.com'; @@ -484,6 +571,7 @@ describe('SelectedNetworkController', () => { it('updates the provider and block tracker proxy when they already exist for the domain', () => { const { controller, mockProviderProxy, mockHasPermissions } = setup({ state: { domains: {} }, + useRequestQueuePreference: true, }); mockHasPermissions.mockReturnValue(true); const initialNetworkClientId = '123'; @@ -515,6 +603,7 @@ describe('SelectedNetworkController', () => { it('throws an error and does not set the networkClientId for the passed in domain', () => { const { controller, mockHasPermissions } = setup({ state: { domains: {} }, + useRequestQueuePreference: true, }); mockHasPermissions.mockReturnValue(false); @@ -529,8 +618,17 @@ describe('SelectedNetworkController', () => { }); }); }); + }); + + describe('getNetworkClientIdForDomain', () => { + it('returns the selectedNetworkClientId from the NetworkController when useRequestQueuePreference is false', () => { + const { controller } = setup(); + expect(controller.getNetworkClientIdForDomain('example.com')).toBe( + 'mainnet', + ); + }); - describe('getNetworkClientIdForDomain', () => { + describe('when useRequestQueuePreference is true', () => { it('returns the networkClientId from state when a networkClientId has been set for the requested domain', () => { const { controller } = setup({ state: { @@ -538,6 +636,7 @@ describe('SelectedNetworkController', () => { 'example.com': '1', }, }, + useRequestQueuePreference: true, }); const result = controller.getNetworkClientIdForDomain('example.com'); @@ -547,286 +646,480 @@ describe('SelectedNetworkController', () => { it('returns the selectedNetworkClientId from the NetworkController when no networkClientId has been set for the requested domain', () => { const { controller } = setup({ state: { domains: {} }, + useRequestQueuePreference: true, }); expect(controller.getNetworkClientIdForDomain('example.com')).toBe( 'mainnet', ); }); }); + }); + + describe('getProviderAndBlockTracker', () => { + it('returns the cached proxy provider and block tracker when the domain already has a cached networkProxy in the domainProxyMap', () => { + const mockProxyProvider = { + setTarget: jest.fn(), + } as unknown as ProviderProxy; + const mockProxyBlockTracker = { + setTarget: jest.fn(), + } as unknown as BlockTrackerProxy; + + const domainProxyMap = new Map([ + [ + 'example.com', + { + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }, + ], + [ + 'test.com', + { + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }, + ], + ]); + const { controller } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: true, + domainProxyMap, + }); + + const result = controller.getProviderAndBlockTracker('example.com'); + expect(result).toStrictEqual({ + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }); + }); - describe('getProviderAndBlockTracker', () => { - it('returns the cached proxy provider and block tracker when the domain already has a cached networkProxy in the domainProxyMap', () => { - const mockProxyProvider = { - setTarget: jest.fn(), - } as unknown as ProviderProxy; - const mockProxyBlockTracker = { - setTarget: jest.fn(), - } as unknown as BlockTrackerProxy; + describe('when the domain does not have a cached networkProxy in the domainProxyMap and useRequestQueuePreference is true', () => { + describe('when the domain has permissions', () => { + it('calls to NetworkController:getNetworkClientById and creates a new proxy provider and block tracker with the non-proxied globally selected network client', () => { + const { controller, messenger, mockHasPermissions } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: true, + }); + jest.spyOn(messenger, 'call'); + mockHasPermissions.mockReturnValue(true); - const domainProxyMap = new Map([ - [ - 'example.com', - { - provider: mockProxyProvider, - blockTracker: mockProxyBlockTracker, + const result = controller.getProviderAndBlockTracker('example.com'); + expect(result).toBeDefined(); + // unfortunately checking which networkController method is called is the best + // proxy (no pun intended) for checking that the correct instance of the networkClient is used + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getNetworkClientById', + 'mainnet', + ); + }); + }); + + describe('when the domain does not have permissions', () => { + it('calls to NetworkController:getSelectedNetworkClient and creates a new proxy provider and block tracker with the proxied globally selected network client', () => { + const { controller, messenger, mockHasPermissions } = setup({ + state: { + domains: {}, }, - ], - [ - 'test.com', - { - provider: mockProxyProvider, - blockTracker: mockProxyBlockTracker, + useRequestQueuePreference: true, + }); + jest.spyOn(messenger, 'call'); + mockHasPermissions.mockReturnValue(false); + const result = controller.getProviderAndBlockTracker('example.com'); + expect(result).toBeDefined(); + // unfortunately checking which networkController method is called is the best + // proxy (no pun intended) for checking that the correct instance of the networkClient is used + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getSelectedNetworkClient', + ); + }); + + it('throws an error if the globally selected network client is not initialized', () => { + const { controller, mockGetSelectedNetworkClient } = setup({ + state: { + domains: {}, }, - ], - ]); - const { controller } = setup({ + useRequestQueuePreference: false, + }); + mockGetSelectedNetworkClient.mockReturnValue(undefined); + expect(() => + controller.getProviderAndBlockTracker('example.com'), + ).toThrow('Selected network not initialized'); + }); + }); + }); + + describe('when the domain does not have a cached networkProxy in the domainProxyMap and useRequestQueuePreference is false', () => { + it('calls to NetworkController:getSelectedNetworkClient and creates a new proxy provider and block tracker with the proxied globally selected network client', () => { + const { controller, messenger } = setup({ state: { domains: {}, }, - - domainProxyMap, + useRequestQueuePreference: false, }); + jest.spyOn(messenger, 'call'); const result = controller.getProviderAndBlockTracker('example.com'); - expect(result).toStrictEqual({ - provider: mockProxyProvider, - blockTracker: mockProxyBlockTracker, + expect(result).toBeDefined(); + // unfortunately checking which networkController method is called is the best + // proxy (no pun intended) for checking that the correct instance of the networkClient is used + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getSelectedNetworkClient', + ); + }); + }); + + // TODO - improve these tests by using a full NetworkController and doing more robust behavioral testing + describe('when the domain is a snap (starts with "npm:" or "local:")', () => { + it('returns a proxied globally selected networkClient and does not create a new proxy in the domainProxyMap', () => { + const { controller, domainProxyMap, messenger } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: true, }); + jest.spyOn(messenger, 'call'); + const snapDomain = 'npm:@metamask/bip32-example-snap'; + + const result = controller.getProviderAndBlockTracker(snapDomain); + + expect(domainProxyMap.get(snapDomain)).toBeUndefined(); + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getSelectedNetworkClient', + ); + expect(result).toBeDefined(); }); - it('throws an error if passed a domain that does not have permissions and the globally selected network client is not initialized', () => { - const { controller, mockGetSelectedNetworkClient, mockHasPermissions } = - setup(); + it('throws an error if the globally selected network client is not initialized', () => { + const { controller, mockGetSelectedNetworkClient } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: false, + }); + const snapDomain = 'npm:@metamask/bip32-example-snap'; mockGetSelectedNetworkClient.mockReturnValue(undefined); - mockHasPermissions.mockReturnValue(false); - expect(() => controller.getProviderAndBlockTracker('test.com')).toThrow( + + expect(() => controller.getProviderAndBlockTracker(snapDomain)).toThrow( 'Selected network not initialized', ); }); + }); - it('throws and error if passed a domain that has permissions and the globally selected network client is not initialized', () => { - const { controller, mockGetNetworkClientById, mockHasPermissions } = - setup(); - mockGetNetworkClientById.mockReturnValue(undefined); - mockHasPermissions.mockReturnValue(true); - expect(() => controller.getProviderAndBlockTracker('test.com')).toThrow( - 'Selected network not initialized', + describe('when the domain is a "metamask"', () => { + it('returns a proxied globally selected networkClient and does not create a new proxy in the domainProxyMap', () => { + const { controller, domainProxyMap, messenger } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: true, + }); + jest.spyOn(messenger, 'call'); + + const result = controller.getProviderAndBlockTracker(METAMASK_DOMAIN); + + expect(result).toBeDefined(); + expect(domainProxyMap.get(METAMASK_DOMAIN)).toBeUndefined(); + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getSelectedNetworkClient', ); }); - describe('when the domain does not have a cached networkProxy in the domainProxyMap', () => { - describe('when the domain has permissions', () => { - it('calls to NetworkController:getNetworkClientById and creates a new proxy provider and block tracker with the non-proxied globally selected network client', () => { - const { controller, messenger, mockHasPermissions } = setup({ - state: { - domains: {}, - }, - }); - jest.spyOn(messenger, 'call'); - mockHasPermissions.mockReturnValue(true); - - const result = controller.getProviderAndBlockTracker('example.com'); - expect(result).toBeDefined(); - // unfortunately checking which networkController method is called is the best - // proxy (no pun intended) for checking that the correct instance of the networkClient is used - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:getNetworkClientById', - 'mainnet', - ); - }); + it('throws an error if the globally selected network client is not initialized', () => { + const { controller, mockGetSelectedNetworkClient } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: false, }); + mockGetSelectedNetworkClient.mockReturnValue(undefined); - describe('when the domain does not have permissions', () => { - it('calls to NetworkController:getSelectedNetworkClient and creates a new proxy provider and block tracker with the proxied globally selected network client', () => { - const { controller, messenger, mockHasPermissions } = setup({ - state: { - domains: {}, - }, - }); - jest.spyOn(messenger, 'call'); - mockHasPermissions.mockReturnValue(false); - const result = controller.getProviderAndBlockTracker('example.com'); - expect(result).toBeDefined(); - // unfortunately checking which networkController method is called is the best - // proxy (no pun intended) for checking that the correct instance of the networkClient is used - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:getSelectedNetworkClient', - ); - }); + expect(() => + controller.getProviderAndBlockTracker(METAMASK_DOMAIN), + ).toThrow('Selected network not initialized'); + }); + }); + }); + + describe('PermissionController:stateChange', () => { + describe('on permission add', () => { + it('should add new domain to domains list when useRequestQueuePreference is true', async () => { + const { controller, messenger } = setup({ + useRequestQueuePreference: true, }); + const mockPermission = { + parentCapability: 'eth_accounts', + id: 'example.com', + date: Date.now(), + caveats: [{ type: 'restrictToAccounts', value: ['0x...'] }], + }; + + messenger.publish( + 'PermissionController:stateChange', + { subjects: {} }, + [ + { + op: 'add', + path: ['subjects', 'example.com', 'permissions'], + value: mockPermission, + }, + ], + ); + + const { domains } = controller.state; + expect(domains['example.com']).toBeDefined(); }); - // TODO - improve these tests by using a full NetworkController and doing more robust behavioral testing - describe('when the domain is a snap (starts with "npm:" or "local:")', () => { - it('returns a proxied globally selected networkClient and does not create a new proxy in the domainProxyMap', () => { - const { controller, domainProxyMap, messenger } = setup({ - state: { - domains: {}, + it('should not add new domain to domains list when useRequestQueuePreference is false', async () => { + const { controller, messenger } = setup({}); + const mockPermission = { + parentCapability: 'eth_accounts', + id: 'example.com', + date: Date.now(), + caveats: [{ type: 'restrictToAccounts', value: ['0x...'] }], + }; + + messenger.publish( + 'PermissionController:stateChange', + { subjects: {} }, + [ + { + op: 'add', + path: ['subjects', 'example.com', 'permissions'], + value: mockPermission, }, - }); - jest.spyOn(messenger, 'call'); - const snapDomain = 'npm:@metamask/bip32-example-snap'; + ], + ); - const result = controller.getProviderAndBlockTracker(snapDomain); + const { domains } = controller.state; + expect(domains['example.com']).toBeUndefined(); + }); + }); - expect(domainProxyMap.get(snapDomain)).toBeUndefined(); - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:getSelectedNetworkClient', - ); - expect(result).toBeDefined(); + describe('on permission removal', () => { + it('should remove domain from domains list', async () => { + const { controller, messenger } = setup({ + state: { domains: { 'example.com': 'foo' } }, }); - it('throws an error if the globally selected network client is not initialized', () => { - const { controller, mockGetSelectedNetworkClient } = setup({ - state: { - domains: {}, + messenger.publish( + 'PermissionController:stateChange', + { subjects: {} }, + [ + { + op: 'remove', + path: ['subjects', 'example.com', 'permissions'], }, - }); - const snapDomain = 'npm:@metamask/bip32-example-snap'; - mockGetSelectedNetworkClient.mockReturnValue(undefined); + ], + ); - expect(() => - controller.getProviderAndBlockTracker(snapDomain), - ).toThrow('Selected network not initialized'); - }); + const { domains } = controller.state; + expect(domains['example.com']).toBeUndefined(); }); - describe('when the domain is a "metamask"', () => { - it('returns a proxied globally selected networkClient and does not create a new proxy in the domainProxyMap', () => { - const { controller, domainProxyMap, messenger } = setup({ - state: { - domains: {}, + it('should set the proxy to the globally selected network if the globally selected network client is initialized and a proxy exists for the domain', async () => { + const { controller, messenger, mockProviderProxy } = setup({ + state: { domains: { 'example.com': 'foo' } }, + }); + controller.getProviderAndBlockTracker('example.com'); + + messenger.publish( + 'PermissionController:stateChange', + { subjects: {} }, + [ + { + op: 'remove', + path: ['subjects', 'example.com', 'permissions'], }, - }); - jest.spyOn(messenger, 'call'); + ], + ); - const result = controller.getProviderAndBlockTracker(METAMASK_DOMAIN); + expect(mockProviderProxy.setTarget).toHaveBeenCalledWith( + expect.objectContaining({ request: expect.any(Function) }), + ); + expect(mockProviderProxy.setTarget).toHaveBeenCalledTimes(1); - expect(result).toBeDefined(); - expect(domainProxyMap.get(METAMASK_DOMAIN)).toBeUndefined(); - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:getSelectedNetworkClient', - ); + const { domains } = controller.state; + expect(domains['example.com']).toBeUndefined(); + }); + + it('should delete the proxy if the globally selected network client is not initialized but a proxy exists for the domain', async () => { + const { + controller, + messenger, + domainProxyMap, + mockProviderProxy, + mockGetSelectedNetworkClient, + } = setup({ + state: { domains: { 'example.com': 'foo' } }, }); + controller.getProviderAndBlockTracker('example.com'); - it('throws an error if the globally selected network client is not initialized', () => { - const { controller, mockGetSelectedNetworkClient } = setup({ - state: { - domains: {}, + mockGetSelectedNetworkClient.mockReturnValue(undefined); + expect(domainProxyMap.get('example.com')).toBeDefined(); + messenger.publish( + 'PermissionController:stateChange', + { subjects: {} }, + [ + { + op: 'remove', + path: ['subjects', 'example.com', 'permissions'], }, - }); - mockGetSelectedNetworkClient.mockReturnValue(undefined); + ], + ); - expect(() => - controller.getProviderAndBlockTracker(METAMASK_DOMAIN), - ).toThrow('Selected network not initialized'); - }); + expect(mockProviderProxy.setTarget).toHaveBeenCalledTimes(0); + expect(domainProxyMap.get('example.com')).toBeUndefined(); }); }); + }); - describe('PermissionController:stateChange', () => { - describe('on permission add', () => { - it('should add new domain to domains list', async () => { - const { controller, messenger } = setup({}); - const mockPermission = { - parentCapability: 'eth_accounts', - id: 'example.com', - date: Date.now(), - caveats: [{ type: 'restrictToAccounts', value: ['0x...'] }], - }; - - messenger.publish( - 'PermissionController:stateChange', - { subjects: {} }, + // because of the opacity of the networkClient and proxy implementations, + // its impossible to make valuable assertions around which networkClient proxies + // should be targeted when the useRequestQueuePreference state is toggled on and off: + // When toggled on, the networkClient for the globally selected networkClientId should be used - **not** the NetworkController's proxy of this networkClient. + // When toggled off, the NetworkControllers proxy of the globally selected networkClient should be used + // TODO - improve these tests by using a full NetworkController and doing more robust behavioral testing + describe('onPreferencesStateChange', () => { + const mockProxyProvider = { + setTarget: jest.fn(), + } as unknown as ProviderProxy; + const mockProxyBlockTracker = { + setTarget: jest.fn(), + } as unknown as BlockTrackerProxy; + + describe('when toggled from off to on', () => { + describe('when domains have permissions', () => { + it('sets the target of the existing proxies to the non-proxied networkClient for the globally selected networkClientId', () => { + const domainProxyMap = new Map([ [ + 'example.com', { - op: 'add', - path: ['subjects', 'example.com', 'permissions'], - value: mockPermission, + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, }, ], - ); + [ + 'test.com', + { + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }, + ], + ]); + + const { + mockHasPermissions, + triggerPreferencesStateChange, + messenger, + } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: false, + domainProxyMap, + }); + jest.spyOn(messenger, 'call'); - const { domains } = controller.state; - expect(domains['example.com']).toBeDefined(); + mockHasPermissions.mockReturnValue(true); + + triggerPreferencesStateChange({ useRequestQueue: true }); + + // this is a very imperfect way to test this, but networkClients and proxies are opaque + // when the proxy is set with the networkClient fetched via NetworkController:getNetworkClientById + // it **is not** tied to the NetworkController's own proxy of the networkClient + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getNetworkClientById', + 'mainnet', + ); + expect(mockProxyProvider.setTarget).toHaveBeenCalledTimes(2); + expect(mockProxyBlockTracker.setTarget).toHaveBeenCalledTimes(2); }); }); - describe('on permission removal', () => { - it('should remove domain from domains list', async () => { - const { controller, messenger } = setup({ - state: { domains: { 'example.com': 'foo' } }, - }); - - messenger.publish( - 'PermissionController:stateChange', - { subjects: {} }, + describe('when domains do not have permissions', () => { + it('does not change the target of the existing proxy', () => { + const domainProxyMap = new Map([ [ + 'example.com', { - op: 'remove', - path: ['subjects', 'example.com', 'permissions'], + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, }, ], - ); - - const { domains } = controller.state; - expect(domains['example.com']).toBeUndefined(); - }); - - it('should set the proxy to the globally selected network if the globally selected network client is initialized and a proxy exists for the domain', async () => { - const { controller, messenger, mockProviderProxy } = setup({ - state: { domains: { 'example.com': 'foo' } }, - }); - controller.getProviderAndBlockTracker('example.com'); - - messenger.publish( - 'PermissionController:stateChange', - { subjects: {} }, [ + 'test.com', { - op: 'remove', - path: ['subjects', 'example.com', 'permissions'], + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, }, ], - ); + ]); + const { mockHasPermissions, triggerPreferencesStateChange } = setup({ + state: { + domains: {}, + }, + useRequestQueuePreference: false, + domainProxyMap, + }); - expect(mockProviderProxy.setTarget).toHaveBeenCalledWith( - expect.objectContaining({ request: expect.any(Function) }), - ); - expect(mockProviderProxy.setTarget).toHaveBeenCalledTimes(1); + mockHasPermissions.mockReturnValue(false); + + triggerPreferencesStateChange({ useRequestQueue: true }); - const { domains } = controller.state; - expect(domains['example.com']).toBeUndefined(); + expect(mockProxyProvider.setTarget).toHaveBeenCalledTimes(0); + expect(mockProxyBlockTracker.setTarget).toHaveBeenCalledTimes(0); }); + }); + }); - it('should delete the proxy if the globally selected network client is not initialized but a proxy exists for the domain', async () => { - const { - controller, - messenger, + describe('when toggled from on to off', () => { + it('sets the target of the existing proxies to the proxied globally selected networkClient', () => { + const domainProxyMap = new Map([ + [ + 'example.com', + { + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }, + ], + [ + 'test.com', + { + provider: mockProxyProvider, + blockTracker: mockProxyBlockTracker, + }, + ], + ]); + + const { mockHasPermissions, triggerPreferencesStateChange, messenger } = + setup({ + state: { + domains: { + 'example.com': 'foo', + 'test.com': 'bar', + }, + }, + useRequestQueuePreference: true, domainProxyMap, - mockProviderProxy, - mockGetSelectedNetworkClient, - } = setup({ - state: { domains: { 'example.com': 'foo' } }, }); - controller.getProviderAndBlockTracker('example.com'); + jest.spyOn(messenger, 'call'); - mockGetSelectedNetworkClient.mockReturnValue(undefined); - expect(domainProxyMap.get('example.com')).toBeDefined(); - messenger.publish( - 'PermissionController:stateChange', - { subjects: {} }, - [ - { - op: 'remove', - path: ['subjects', 'example.com', 'permissions'], - }, - ], - ); + mockHasPermissions.mockReturnValue(true); - expect(mockProviderProxy.setTarget).toHaveBeenCalledTimes(0); - expect(domainProxyMap.get('example.com')).toBeUndefined(); - }); + triggerPreferencesStateChange({ useRequestQueue: false }); + + // this is a very imperfect way to test this, but networkClients and proxies are opaque + // when the proxy is set with the networkClient fetched via NetworkController:getSelectedNetworkClient + // it **is** tied to the NetworkController's own proxy of the networkClient + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:getSelectedNetworkClient', + ); + expect(mockProxyProvider.setTarget).toHaveBeenCalledTimes(2); + expect(mockProxyBlockTracker.setTarget).toHaveBeenCalledTimes(2); }); }); }); From e73bc47e180afa1d59784fac865651f1941de8fa Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 13 Dec 2024 18:31:44 -0330 Subject: [PATCH 18/23] chore: Rename `ControllerMessenger` to `Messenger` (#5050) ## Explanation Rename the `ControllerMessenger` to `Messenger` so that it is clear it can be used for more than just controllers. This was decided by this ADR: https://github.com/MetaMask/decisions/blob/main/decisions/core/0001-messaging-non-controllers.md Previous names have been preserved as aliases to avoid making this a breaking change. ## References Relates to #4538 ## Changelog See diff ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/base-controller/CHANGELOG.md | 9 + .../src/BaseControllerV1.test.ts | 2 +- .../src/BaseControllerV2.test.ts | 4 +- .../base-controller/src/BaseControllerV2.ts | 4 +- ...lerMessenger.test.ts => Messenger.test.ts} | 311 ++++---- .../{ControllerMessenger.ts => Messenger.ts} | 40 +- ...er.test.ts => RestrictedMessenger.test.ts} | 662 ++++++++---------- ...lerMessenger.ts => RestrictedMessenger.ts} | 127 ++-- packages/base-controller/src/index.ts | 14 +- 9 files changed, 544 insertions(+), 629 deletions(-) rename packages/base-controller/src/{ControllerMessenger.test.ts => Messenger.test.ts} (58%) rename packages/base-controller/src/{ControllerMessenger.ts => Messenger.ts} (92%) rename packages/base-controller/src/{RestrictedControllerMessenger.test.ts => RestrictedMessenger.test.ts} (60%) rename packages/base-controller/src/{RestrictedControllerMessenger.ts => RestrictedMessenger.ts} (80%) diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index b8ce86fe45..374666e8fd 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Rename `ControllerMessenger` to `Messenger` ([#5050](https://github.com/MetaMask/core/pull/5050)) + - `ControllerMessenger` has been renamed to `Messenger` + - `RestrictedControllerMessengerConstraint` has been renamed to `RestrictedMessengerConstraint` + - `RestrictedControllerMessenger` has been renamed to `RestrictedMessenger` + - The `RestrictedMessenger` constructor parameter `controllerMessenger` has been renamed to `messenger`, though the old name is still accepted + - The old names remain exported as deprecated aliases of the new names, so this is not a breaking change. + ## [7.0.2] ### Changed diff --git a/packages/base-controller/src/BaseControllerV1.test.ts b/packages/base-controller/src/BaseControllerV1.test.ts index f268f0f4a3..57a71b589f 100644 --- a/packages/base-controller/src/BaseControllerV1.test.ts +++ b/packages/base-controller/src/BaseControllerV1.test.ts @@ -16,7 +16,7 @@ import { countControllerStateMetadata, getCountMessenger, } from './BaseControllerV2.test'; -import { ControllerMessenger } from './ControllerMessenger'; +import { ControllerMessenger } from './Messenger'; const STATE = { name: 'foo' }; const CONFIG = { disabled: true }; diff --git a/packages/base-controller/src/BaseControllerV2.test.ts b/packages/base-controller/src/BaseControllerV2.test.ts index 9227505090..fc9f06516f 100644 --- a/packages/base-controller/src/BaseControllerV2.test.ts +++ b/packages/base-controller/src/BaseControllerV2.test.ts @@ -14,8 +14,8 @@ import { getPersistentState, isBaseController, } from './BaseControllerV2'; -import { ControllerMessenger } from './ControllerMessenger'; -import type { RestrictedControllerMessenger } from './RestrictedControllerMessenger'; +import { ControllerMessenger } from './Messenger'; +import type { RestrictedControllerMessenger } from './RestrictedMessenger'; export const countControllerName = 'CountController'; diff --git a/packages/base-controller/src/BaseControllerV2.ts b/packages/base-controller/src/BaseControllerV2.ts index 355d156689..1dd2bb05b4 100644 --- a/packages/base-controller/src/BaseControllerV2.ts +++ b/packages/base-controller/src/BaseControllerV2.ts @@ -6,11 +6,11 @@ import type { BaseControllerV1Instance, StateConstraint as StateConstraintV1, } from './BaseControllerV1'; -import type { ActionConstraint, EventConstraint } from './ControllerMessenger'; +import type { ActionConstraint, EventConstraint } from './Messenger'; import type { RestrictedControllerMessenger, RestrictedControllerMessengerConstraint, -} from './RestrictedControllerMessenger'; +} from './RestrictedMessenger'; enablePatches(); diff --git a/packages/base-controller/src/ControllerMessenger.test.ts b/packages/base-controller/src/Messenger.test.ts similarity index 58% rename from packages/base-controller/src/ControllerMessenger.test.ts rename to packages/base-controller/src/Messenger.test.ts index 6dec60d1fb..fe00e91e84 100644 --- a/packages/base-controller/src/ControllerMessenger.test.ts +++ b/packages/base-controller/src/Messenger.test.ts @@ -1,30 +1,29 @@ import type { Patch } from 'immer'; import * as sinon from 'sinon'; -import { ControllerMessenger } from './ControllerMessenger'; +import { Messenger } from './Messenger'; -describe('ControllerMessenger', () => { +describe('Messenger', () => { afterEach(() => { sinon.restore(); }); it('should allow registering and calling an action handler', () => { type CountAction = { type: 'count'; handler: (increment: number) => void }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); let count = 0; - controllerMessenger.registerActionHandler('count', (increment: number) => { + messenger.registerActionHandler('count', (increment: number) => { count += increment; }); - controllerMessenger.call('count', 1); + messenger.call('count', 1); expect(count).toBe(1); }); it('should allow registering and calling multiple different action handlers', () => { - // These 'Other' types are included to demonstrate that controller messenger - // generics can indeed be unions of actions and events from different - // controllers. + // These 'Other' types are included to demonstrate that messenger generics can indeed be unions + // of actions and events from different modules. type GetOtherState = { type: `OtherController:getState`; handler: () => { stuff: string }; @@ -38,41 +37,35 @@ describe('ControllerMessenger', () => { type MessageAction = | { type: 'concat'; handler: (message: string) => void } | { type: 'reset'; handler: (initialMessage: string) => void }; - const controllerMessenger = new ControllerMessenger< + const messenger = new Messenger< MessageAction | GetOtherState, OtherStateChange >(); let message = ''; - controllerMessenger.registerActionHandler( - 'reset', - (initialMessage: string) => { - message = initialMessage; - }, - ); + messenger.registerActionHandler('reset', (initialMessage: string) => { + message = initialMessage; + }); - controllerMessenger.registerActionHandler('concat', (s: string) => { + messenger.registerActionHandler('concat', (s: string) => { message += s; }); - controllerMessenger.call('reset', 'hello'); - controllerMessenger.call('concat', ', world'); + messenger.call('reset', 'hello'); + messenger.call('concat', ', world'); expect(message).toBe('hello, world'); }); it('should allow registering and calling an action handler with no parameters', () => { type IncrementAction = { type: 'increment'; handler: () => void }; - const controllerMessenger = new ControllerMessenger< - IncrementAction, - never - >(); + const messenger = new Messenger(); let count = 0; - controllerMessenger.registerActionHandler('increment', () => { + messenger.registerActionHandler('increment', () => { count += 1; }); - controllerMessenger.call('increment'); + messenger.call('increment'); expect(count).toBe(1); }); @@ -82,98 +75,98 @@ describe('ControllerMessenger', () => { type: 'message'; handler: (to: string, message: string) => void; }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const messages: Record = {}; - controllerMessenger.registerActionHandler('message', (to, message) => { + messenger.registerActionHandler('message', (to, message) => { messages[to] = message; }); - controllerMessenger.call('message', '0x123', 'hello'); + messenger.call('message', '0x123', 'hello'); expect(messages['0x123']).toBe('hello'); }); it('should allow registering and calling an action handler with a return value', () => { type AddAction = { type: 'add'; handler: (a: number, b: number) => number }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); - controllerMessenger.registerActionHandler('add', (a, b) => { + messenger.registerActionHandler('add', (a, b) => { return a + b; }); - const result = controllerMessenger.call('add', 5, 10); + const result = messenger.call('add', 5, 10); expect(result).toBe(15); }); it('should not allow registering multiple action handlers under the same name', () => { type PingAction = { type: 'ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); - controllerMessenger.registerActionHandler('ping', () => undefined); + messenger.registerActionHandler('ping', () => undefined); expect(() => { - controllerMessenger.registerActionHandler('ping', () => undefined); + messenger.registerActionHandler('ping', () => undefined); }).toThrow('A handler for ping has already been registered'); }); it('should throw when calling unregistered action', () => { type PingAction = { type: 'ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); expect(() => { - controllerMessenger.call('ping'); + messenger.call('ping'); }).toThrow('A handler for ping has not been registered'); }); it('should throw when calling an action that has been unregistered', () => { type PingAction = { type: 'ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); expect(() => { - controllerMessenger.call('ping'); + messenger.call('ping'); }).toThrow('A handler for ping has not been registered'); let pingCount = 0; - controllerMessenger.registerActionHandler('ping', () => { + messenger.registerActionHandler('ping', () => { pingCount += 1; }); - controllerMessenger.unregisterActionHandler('ping'); + messenger.unregisterActionHandler('ping'); expect(() => { - controllerMessenger.call('ping'); + messenger.call('ping'); }).toThrow('A handler for ping has not been registered'); expect(pingCount).toBe(0); }); it('should throw when calling an action after actions have been reset', () => { type PingAction = { type: 'ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); expect(() => { - controllerMessenger.call('ping'); + messenger.call('ping'); }).toThrow('A handler for ping has not been registered'); let pingCount = 0; - controllerMessenger.registerActionHandler('ping', () => { + messenger.registerActionHandler('ping', () => { pingCount += 1; }); - controllerMessenger.clearActions(); + messenger.clearActions(); expect(() => { - controllerMessenger.call('ping'); + messenger.call('ping'); }).toThrow('A handler for ping has not been registered'); expect(pingCount).toBe(0); }); it('should publish event to subscriber', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler); + messenger.publish('message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -183,15 +176,15 @@ describe('ControllerMessenger', () => { type MessageEvent = | { type: 'message'; payload: [string] } | { type: 'ping'; payload: [] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const messageHandler = sinon.stub(); const pingHandler = sinon.stub(); - controllerMessenger.subscribe('message', messageHandler); - controllerMessenger.subscribe('ping', pingHandler); + messenger.subscribe('message', messageHandler); + messenger.subscribe('ping', pingHandler); - controllerMessenger.publish('message', 'hello'); - controllerMessenger.publish('ping'); + messenger.publish('message', 'hello'); + messenger.publish('ping'); expect(messageHandler.calledWithExactly('hello')).toBe(true); expect(messageHandler.callCount).toBe(1); @@ -201,11 +194,11 @@ describe('ControllerMessenger', () => { it('should publish event with no payload to subscriber', () => { type PingEvent = { type: 'ping'; payload: [] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('ping', handler); - controllerMessenger.publish('ping'); + messenger.subscribe('ping', handler); + messenger.publish('ping'); expect(handler.calledWithExactly()).toBe(true); expect(handler.callCount).toBe(1); @@ -213,11 +206,11 @@ describe('ControllerMessenger', () => { it('should publish event with multiple payload parameters to subscriber', () => { type MessageEvent = { type: 'message'; payload: [string, string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.publish('message', 'hello', 'there'); + messenger.subscribe('message', handler); + messenger.publish('message', 'hello', 'there'); expect(handler.calledWithExactly('hello', 'there')).toBe(true); expect(handler.callCount).toBe(1); @@ -225,12 +218,12 @@ describe('ControllerMessenger', () => { it('should publish event once to subscriber even if subscribed multiple times', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.subscribe('message', handler); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler); + messenger.subscribe('message', handler); + messenger.publish('message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -238,13 +231,13 @@ describe('ControllerMessenger', () => { it('should publish event to many subscribers', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler1 = sinon.stub(); const handler2 = sinon.stub(); - controllerMessenger.subscribe('message', handler1); - controllerMessenger.subscribe('message', handler2); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler1); + messenger.subscribe('message', handler2); + messenger.publish('message', 'hello'); expect(handler1.calledWithExactly('hello')).toBe(true); expect(handler1.callCount).toBe(1); @@ -262,23 +255,16 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - controllerMessenger.registerInitialEventPayload({ + const messenger = new Messenger(); + messenger.registerInitialEventPayload({ eventType: 'complexMessage', getPayload: () => [state], }); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.propA, - ); + messenger.subscribe('complexMessage', handler, (obj) => obj.propA); state.propA += 1; - controllerMessenger.publish('complexMessage', state); + messenger.publish('complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([2, 1]); expect(handler.callCount).toBe(1); @@ -293,22 +279,15 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - controllerMessenger.registerInitialEventPayload({ + const messenger = new Messenger(); + messenger.registerInitialEventPayload({ eventType: 'complexMessage', getPayload: () => [state], }); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.propA, - ); + messenger.subscribe('complexMessage', handler, (obj) => obj.propA); - controllerMessenger.publish('complexMessage', state); + messenger.publish('complexMessage', state); expect(handler.callCount).toBe(0); }); @@ -324,19 +303,12 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.propA, - ); + messenger.subscribe('complexMessage', handler, (obj) => obj.propA); state.propA += 1; - controllerMessenger.publish('complexMessage', state); + messenger.publish('complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([2, undefined]); expect(handler.callCount).toBe(1); @@ -351,18 +323,11 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.propA, - ); + messenger.subscribe('complexMessage', handler, (obj) => obj.propA); - controllerMessenger.publish('complexMessage', state); + messenger.publish('complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([1, undefined]); expect(handler.callCount).toBe(1); @@ -377,18 +342,11 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.propA, - ); + messenger.subscribe('complexMessage', handler, (obj) => obj.propA); - controllerMessenger.publish('complexMessage', state); + messenger.publish('complexMessage', state); expect(handler.callCount).toBe(0); }); @@ -400,19 +358,12 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.prop1, - ); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); - controllerMessenger.publish('complexMessage', { prop1: 'z', prop2: 'b' }); + messenger.subscribe('complexMessage', handler, (obj) => obj.prop1); + messenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); + messenger.publish('complexMessage', { prop1: 'z', prop2: 'b' }); expect(handler.getCall(0).calledWithExactly('a', undefined)).toBe(true); expect(handler.getCall(1).calledWithExactly('z', 'a')).toBe(true); @@ -424,18 +375,11 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.prop1, - ); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); + messenger.subscribe('complexMessage', handler, (obj) => obj.prop1); + messenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); expect(handler.calledWithExactly('a', undefined)).toBe(true); expect(handler.callCount).toBe(1); @@ -446,19 +390,12 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe( - 'complexMessage', - handler, - (obj) => obj.prop1, - ); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop3: 'c' }); + messenger.subscribe('complexMessage', handler, (obj) => obj.prop1); + messenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); + messenger.publish('complexMessage', { prop1: 'a', prop3: 'c' }); expect(handler.calledWithExactly('a', undefined)).toBe(true); expect(handler.callCount).toBe(1); @@ -470,15 +407,15 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler1 = sinon.stub(); const handler2 = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.prop1); - controllerMessenger.subscribe('complexMessage', handler1, selector); - controllerMessenger.subscribe('complexMessage', handler2, selector); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop3: 'c' }); + messenger.subscribe('complexMessage', handler1, selector); + messenger.subscribe('complexMessage', handler2, selector); + messenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); + messenger.publish('complexMessage', { prop1: 'a', prop3: 'c' }); expect(handler1.calledWithExactly('a', undefined)).toBe(true); expect(handler1.callCount).toBe(1); @@ -505,12 +442,12 @@ describe('ControllerMessenger', () => { it('should throw subscriber errors in a timeout', () => { const setTimeoutStub = sinon.stub(globalThis, 'setTimeout'); type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub().throws(() => new Error('Example error')); - controllerMessenger.subscribe('message', handler); + messenger.subscribe('message', handler); - expect(() => controllerMessenger.publish('message', 'hello')).not.toThrow(); + expect(() => messenger.publish('message', 'hello')).not.toThrow(); expect(setTimeoutStub.callCount).toBe(1); const onTimeout = setTimeoutStub.firstCall.args[0]; expect(() => onTimeout()).toThrow('Example error'); @@ -519,14 +456,14 @@ describe('ControllerMessenger', () => { it('should continue calling subscribers when one throws', () => { const setTimeoutStub = sinon.stub(globalThis, 'setTimeout'); type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler1 = sinon.stub().throws(() => new Error('Example error')); const handler2 = sinon.stub(); - controllerMessenger.subscribe('message', handler1); - controllerMessenger.subscribe('message', handler2); + messenger.subscribe('message', handler1); + messenger.subscribe('message', handler2); - expect(() => controllerMessenger.publish('message', 'hello')).not.toThrow(); + expect(() => messenger.publish('message', 'hello')).not.toThrow(); expect(handler1.calledWithExactly('hello')).toBe(true); expect(handler1.callCount).toBe(1); @@ -539,12 +476,12 @@ describe('ControllerMessenger', () => { it('should not call subscriber after unsubscribing', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.unsubscribe('message', handler); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler); + messenger.unsubscribe('message', handler); + messenger.publish('message', 'hello'); expect(handler.callCount).toBe(0); }); @@ -554,13 +491,13 @@ describe('ControllerMessenger', () => { type: 'complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.prop1); - controllerMessenger.subscribe('complexMessage', handler, selector); - controllerMessenger.unsubscribe('complexMessage', handler); - controllerMessenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); + messenger.subscribe('complexMessage', handler, selector); + messenger.unsubscribe('complexMessage', handler); + messenger.publish('complexMessage', { prop1: 'a', prop2: 'b' }); expect(handler.callCount).toBe(0); expect(selector.callCount).toBe(0); @@ -568,56 +505,54 @@ describe('ControllerMessenger', () => { it('should throw when unsubscribing when there are no subscriptions', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - expect(() => controllerMessenger.unsubscribe('message', handler)).toThrow( + expect(() => messenger.unsubscribe('message', handler)).toThrow( 'Subscription not found for event: message', ); }); it('should throw when unsubscribing a handler that is not subscribed', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler1 = sinon.stub(); const handler2 = sinon.stub(); - controllerMessenger.subscribe('message', handler1); + messenger.subscribe('message', handler1); - expect(() => controllerMessenger.unsubscribe('message', handler2)).toThrow( + expect(() => messenger.unsubscribe('message', handler2)).toThrow( 'Subscription not found for event: message', ); }); it('should not call subscriber after clearing event subscriptions', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.clearEventSubscriptions('message'); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler); + messenger.clearEventSubscriptions('message'); + messenger.publish('message', 'hello'); expect(handler.callCount).toBe(0); }); it('should not throw when clearing event that has no subscriptions', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); - expect(() => - controllerMessenger.clearEventSubscriptions('message'), - ).not.toThrow(); + expect(() => messenger.clearEventSubscriptions('message')).not.toThrow(); }); it('should not call subscriber after resetting subscriptions', () => { type MessageEvent = { type: 'message'; payload: [string] }; - const controllerMessenger = new ControllerMessenger(); + const messenger = new Messenger(); const handler = sinon.stub(); - controllerMessenger.subscribe('message', handler); - controllerMessenger.clearSubscriptions(); - controllerMessenger.publish('message', 'hello'); + messenger.subscribe('message', handler); + messenger.clearSubscriptions(); + messenger.publish('message', 'hello'); expect(handler.callCount).toBe(0); }); diff --git a/packages/base-controller/src/ControllerMessenger.ts b/packages/base-controller/src/Messenger.ts similarity index 92% rename from packages/base-controller/src/ControllerMessenger.ts rename to packages/base-controller/src/Messenger.ts index ea524bd3e6..4d10201913 100644 --- a/packages/base-controller/src/ControllerMessenger.ts +++ b/packages/base-controller/src/Messenger.ts @@ -1,4 +1,4 @@ -import { RestrictedControllerMessenger } from './RestrictedControllerMessenger'; +import { RestrictedMessenger } from './RestrictedMessenger'; export type ActionHandler< Action extends ActionConstraint, @@ -114,16 +114,16 @@ type NarrowToAllowed = Name extends { : never; /** - * A messaging system for controllers. + * A message broker for "actions" and "events". * - * The controller messenger allows registering functions as 'actions' that can be called elsewhere, + * The messenger allows registering functions as 'actions' that can be called elsewhere, * and it allows publishing and subscribing to events. Both actions and events are identified by * unique strings. * * @template Action - A type union of all Action types. * @template Event - A type union of all Event types. */ -export class ControllerMessenger< +export class Messenger< Action extends ActionConstraint, Event extends EventConstraint, > { @@ -399,30 +399,30 @@ export class ControllerMessenger< } /** - * Get a restricted controller messenger + * Get a restricted messenger * - * Returns a wrapper around the controller messenger instance that restricts access to actions - * and events. The provided allowlists grant the ability to call the listed actions and subscribe - * to the listed events. The "name" provided grants ownership of any actions and events under - * that namespace. Ownership allows registering actions and publishing events, as well as + * Returns a wrapper around the messenger instance that restricts access to actions and events. + * The provided allowlists grant the ability to call the listed actions and subscribe to the + * listed events. The "name" provided grants ownership of any actions and events under that + * namespace. Ownership allows registering actions and publishing events, as well as * unregistering actions and clearing event subscriptions. * * @param options - Controller messenger options. * @param options.name - The name of the thing this messenger will be handed to (e.g. the * controller name). This grants "ownership" of actions and events under this namespace to the - * restricted controller messenger returned. - * @param options.allowedActions - The list of actions that this restricted controller messenger - * should be alowed to call. - * @param options.allowedEvents - The list of events that this restricted controller messenger - * should be allowed to subscribe to. - * @template Namespace - The namespace for this messenger. Typically this is the name of the controller or + * restricted messenger returned. + * @param options.allowedActions - The list of actions that this restricted messenger should be + * allowed to call. + * @param options.allowedEvents - The list of events that this restricted messenger should be + * allowed to subscribe to. + * @template Namespace - The namespace for this messenger. Typically this is the name of the * module that this messenger has been created for. The authority to publish events and register * actions under this namespace is granted to this restricted messenger instance. * @template AllowedAction - A type union of the 'type' string for any allowed actions. * This must not include internal actions that are in the messenger's namespace. * @template AllowedEvent - A type union of the 'type' string for any allowed events. * This must not include internal events that are in the messenger's namespace. - * @returns The restricted controller messenger. + * @returns The restricted messenger. */ getRestricted< Namespace extends string, @@ -442,7 +442,7 @@ export class ControllerMessenger< Namespace, Extract >[]; - }): RestrictedControllerMessenger< + }): RestrictedMessenger< Namespace, | NarrowToNamespace | NarrowToAllowed, @@ -450,11 +450,13 @@ export class ControllerMessenger< AllowedAction, AllowedEvent > { - return new RestrictedControllerMessenger({ - controllerMessenger: this, + return new RestrictedMessenger({ + messenger: this, name, allowedActions, allowedEvents, }); } } + +export { Messenger as ControllerMessenger }; diff --git a/packages/base-controller/src/RestrictedControllerMessenger.test.ts b/packages/base-controller/src/RestrictedMessenger.test.ts similarity index 60% rename from packages/base-controller/src/RestrictedControllerMessenger.test.ts rename to packages/base-controller/src/RestrictedMessenger.test.ts index e4273e6729..c0c2a66115 100644 --- a/packages/base-controller/src/RestrictedControllerMessenger.test.ts +++ b/packages/base-controller/src/RestrictedMessenger.test.ts @@ -1,28 +1,121 @@ import * as sinon from 'sinon'; -import { ControllerMessenger } from './ControllerMessenger'; +import { Messenger } from './Messenger'; +import { RestrictedMessenger } from './RestrictedMessenger'; + +describe('RestrictedMessenger', () => { + describe('constructor', () => { + it('should throw if no messenger is provided', () => { + expect( + () => + new RestrictedMessenger({ + name: 'Test', + allowedActions: [], + allowedEvents: [], + }), + ).toThrow('Messenger not provided'); + }); + + it('should throw if both controllerMessenger and messenger are provided', () => { + const messenger = new Messenger(); + + expect( + () => + new RestrictedMessenger({ + controllerMessenger: messenger, + messenger, + name: 'Test', + allowedActions: [], + allowedEvents: [], + }), + ).toThrow( + `Both messenger properties provided. Provide message using only 'messenger' option, 'controllerMessenger' is deprecated`, + ); + }); + + it('should accept messenger parameter', () => { + type CountAction = { + type: 'CountController:count'; + handler: (increment: number) => void; + }; + const messenger = new Messenger(); + const restrictedMessenger = new RestrictedMessenger< + 'CountController', + CountAction, + never, + never, + never + >({ + messenger, + name: 'CountController', + allowedActions: [], + allowedEvents: [], + }); + + let count = 0; + restrictedMessenger.registerActionHandler( + 'CountController:count', + (increment: number) => { + count += increment; + }, + ); + restrictedMessenger.call('CountController:count', 1); + + expect(count).toBe(1); + }); + + it('should accept controllerMessenger parameter', () => { + type CountAction = { + type: 'CountController:count'; + handler: (increment: number) => void; + }; + const messenger = new Messenger(); + const restrictedMessenger = new RestrictedMessenger< + 'CountController', + CountAction, + never, + never, + never + >({ + controllerMessenger: messenger, + name: 'CountController', + allowedActions: [], + allowedEvents: [], + }); + + let count = 0; + restrictedMessenger.registerActionHandler( + 'CountController:count', + (increment: number) => { + count += increment; + }, + ); + restrictedMessenger.call('CountController:count', 1); + + expect(count).toBe(1); + }); + }); -describe('RestrictedControllerMessenger', () => { it('should allow registering and calling an action handler', () => { type CountAction = { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); let count = 0; - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'CountController:count', (increment: number) => { count += increment; }, ); - restrictedControllerMessenger.call('CountController:count', 1); + restrictedMessenger.call('CountController:count', 1); expect(count).toBe(1); }); @@ -34,30 +127,30 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:reset'; handler: (initialMessage: string) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); let message = ''; - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'MessageController:reset', (initialMessage: string) => { message = initialMessage; }, ); - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'MessageController:concat', (s: string) => { message += s; }, ); - restrictedControllerMessenger.call('MessageController:reset', 'hello'); - restrictedControllerMessenger.call('MessageController:concat', ', world'); + restrictedMessenger.call('MessageController:reset', 'hello'); + restrictedMessenger.call('MessageController:concat', ', world'); expect(message).toBe('hello, world'); }); @@ -67,24 +160,21 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:increment'; handler: () => void; }; - const controllerMessenger = new ControllerMessenger< - IncrementAction, - never - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); let count = 0; - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'CountController:increment', () => { count += 1; }, ); - restrictedControllerMessenger.call('CountController:increment'); + restrictedMessenger.call('CountController:increment'); expect(count).toBe(1); }); @@ -94,26 +184,22 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; handler: (to: string, message: string) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const messages: Record = {}; - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'MessageController:message', (to, message) => { messages[to] = message; }, ); - restrictedControllerMessenger.call( - 'MessageController:message', - '0x123', - 'hello', - ); + restrictedMessenger.call('MessageController:message', '0x123', 'hello'); expect(messages['0x123']).toBe('hello'); }); @@ -123,44 +209,37 @@ describe('RestrictedControllerMessenger', () => { type: 'MathController:add'; handler: (a: number, b: number) => number; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MathController', allowedActions: [], allowedEvents: [], }); - restrictedControllerMessenger.registerActionHandler( - 'MathController:add', - (a, b) => { - return a + b; - }, - ); - const result = restrictedControllerMessenger.call( - 'MathController:add', - 5, - 10, - ); + restrictedMessenger.registerActionHandler('MathController:add', (a, b) => { + return a + b; + }); + const result = restrictedMessenger.call('MathController:add', 5, 10); expect(result).toBe(15); }); it('should not allow registering multiple action handlers under the same name', () => { type CountAction = { type: 'PingController:ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'PingController', allowedActions: [], allowedEvents: [], }); - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'PingController:ping', () => undefined, ); expect(() => { - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'PingController:ping', () => undefined, ); @@ -172,15 +251,15 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( // @ts-expect-error: suppressing to test runtime error handling 'OtherController:other', () => undefined, @@ -195,15 +274,15 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( // @ts-expect-error: suppressing to test runtime error handling 'OtherController:other', () => undefined, @@ -220,18 +299,15 @@ describe('RestrictedControllerMessenger', () => { type: 'OtherController:other'; payload: [unknown]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent | OtherEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: ['OtherController:other'], }); expect(() => { - restrictedControllerMessenger.publish( + restrictedMessenger.publish( // @ts-expect-error: suppressing to test runtime error handling 'OtherController:other', () => undefined, @@ -246,15 +322,15 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.unsubscribe( + restrictedMessenger.unsubscribe( // @ts-expect-error: suppressing to test runtime error handling 'OtherController:other', () => undefined, @@ -271,18 +347,15 @@ describe('RestrictedControllerMessenger', () => { type: 'OtherController:other'; payload: [unknown]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent | OtherEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: ['OtherController:other'], }); expect(() => { - restrictedControllerMessenger.clearEventSubscriptions( + restrictedMessenger.clearEventSubscriptions( // @ts-expect-error: suppressing to test runtime error handling 'OtherController:other', ); @@ -294,8 +367,8 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'PingController', allowedActions: [], allowedEvents: [], @@ -303,7 +376,7 @@ describe('RestrictedControllerMessenger', () => { expect(() => { // @ts-expect-error suppressing to test runtime error handling - restrictedControllerMessenger.call('CountController:count'); + restrictedMessenger.call('CountController:count'); }).toThrow('Action missing from allow list: CountController:count'); }); @@ -312,8 +385,8 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted< + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted< 'PingController', CountAction['type'] >({ @@ -323,7 +396,7 @@ describe('RestrictedControllerMessenger', () => { }); expect(() => { - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( // @ts-expect-error suppressing to test runtime error handling 'CountController:count', () => undefined, @@ -338,8 +411,8 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted< + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted< 'PingController', CountAction['type'] >({ @@ -348,7 +421,7 @@ describe('RestrictedControllerMessenger', () => { allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.unregisterActionHandler( + restrictedMessenger.unregisterActionHandler( // @ts-expect-error suppressing to test runtime error handling 'CountController:count', ); @@ -359,45 +432,40 @@ describe('RestrictedControllerMessenger', () => { it('should throw when calling unregistered action', () => { type PingAction = { type: 'PingController:ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'PingController', allowedActions: [], allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.call('PingController:ping'); + restrictedMessenger.call('PingController:ping'); }).toThrow('A handler for PingController:ping has not been registered'); }); it('should throw when calling an action that has been unregistered', () => { type PingAction = { type: 'PingController:ping'; handler: () => void }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'PingController', allowedActions: [], allowedEvents: [], }); expect(() => { - restrictedControllerMessenger.call('PingController:ping'); + restrictedMessenger.call('PingController:ping'); }).toThrow('A handler for PingController:ping has not been registered'); let pingCount = 0; - restrictedControllerMessenger.registerActionHandler( - 'PingController:ping', - () => { - pingCount += 1; - }, - ); + restrictedMessenger.registerActionHandler('PingController:ping', () => { + pingCount += 1; + }); - restrictedControllerMessenger.unregisterActionHandler( - 'PingController:ping', - ); + restrictedMessenger.unregisterActionHandler('PingController:ping'); expect(() => { - restrictedControllerMessenger.call('PingController:ping'); + restrictedMessenger.call('PingController:ping'); }).toThrow('A handler for PingController:ping has not been registered'); expect(pingCount).toBe(0); }); @@ -407,15 +475,15 @@ describe('RestrictedControllerMessenger', () => { type: 'OtherController:complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); expect(() => - restrictedControllerMessenger.registerInitialEventPayload({ + restrictedMessenger.registerInitialEventPayload({ // @ts-expect-error suppressing to test runtime error handling eventType: 'OtherController:complexMessage', // @ts-expect-error suppressing to test runtime error handling @@ -431,19 +499,16 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.subscribe('MessageController:message', handler); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -459,32 +524,26 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); - restrictedControllerMessenger.registerInitialEventPayload({ + restrictedMessenger.registerInitialEventPayload({ eventType: 'MessageController:complexMessage', getPayload: () => [state], }); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.propA); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); state.propA += 1; - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - state, - ); + restrictedMessenger.publish('MessageController:complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([2, 1]); expect(handler.callCount).toBe(1); @@ -499,31 +558,25 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); - restrictedControllerMessenger.registerInitialEventPayload({ + restrictedMessenger.registerInitialEventPayload({ eventType: 'MessageController:complexMessage', getPayload: () => [state], }); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.propA); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - state, - ); + restrictedMessenger.publish('MessageController:complexMessage', state); expect(handler.callCount).toBe(0); }); @@ -539,28 +592,22 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.propA); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); state.propA += 1; - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - state, - ); + restrictedMessenger.publish('MessageController:complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([2, undefined]); expect(handler.callCount).toBe(1); @@ -575,27 +622,21 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.propA); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - state, - ); + restrictedMessenger.publish('MessageController:complexMessage', state); expect(handler.getCall(0)?.args).toStrictEqual([1, undefined]); expect(handler.callCount).toBe(1); @@ -610,27 +651,21 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [typeof state]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.propA); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - state, - ); + restrictedMessenger.publish('MessageController:complexMessage', state); expect(handler.callCount).toBe(0); }); @@ -642,11 +677,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -654,25 +686,19 @@ describe('RestrictedControllerMessenger', () => { const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.prop1); - controllerMessenger.subscribe( + messenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - { - prop1: 'a', - prop2: 'b', - }, - ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - { - prop1: 'z', - prop2: 'b', - }, - ); + restrictedMessenger.publish('MessageController:complexMessage', { + prop1: 'a', + prop2: 'b', + }); + restrictedMessenger.publish('MessageController:complexMessage', { + prop1: 'z', + prop2: 'b', + }); expect(handler.getCall(0).calledWithExactly('a', undefined)).toBe(true); expect(handler.getCall(1).calledWithExactly('z', 'a')).toBe(true); @@ -684,11 +710,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -696,18 +719,15 @@ describe('RestrictedControllerMessenger', () => { const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.prop1); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - { - prop1: 'a', - prop2: 'b', - }, - ); + restrictedMessenger.publish('MessageController:complexMessage', { + prop1: 'a', + prop2: 'b', + }); expect(handler.calledWithExactly('a', undefined)).toBe(true); expect(handler.callCount).toBe(1); @@ -718,11 +738,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:complexMessage'; payload: [Record]; }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent - >(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -730,25 +747,19 @@ describe('RestrictedControllerMessenger', () => { const handler = sinon.stub(); const selector = sinon.fake((obj: Record) => obj.prop1); - restrictedControllerMessenger.subscribe( + restrictedMessenger.subscribe( 'MessageController:complexMessage', handler, selector, ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - { - prop1: 'a', - prop2: 'b', - }, - ); - restrictedControllerMessenger.publish( - 'MessageController:complexMessage', - { - prop1: 'a', - prop3: 'c', - }, - ); + restrictedMessenger.publish('MessageController:complexMessage', { + prop1: 'a', + prop2: 'b', + }); + restrictedMessenger.publish('MessageController:complexMessage', { + prop1: 'a', + prop3: 'c', + }); expect(handler.calledWithExactly('a', undefined)).toBe(true); expect(handler.callCount).toBe(1); @@ -759,8 +770,8 @@ describe('RestrictedControllerMessenger', () => { type MessageEvent = | { type: 'MessageController:message'; payload: [string] } | { type: 'MessageController:ping'; payload: [] }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -768,18 +779,12 @@ describe('RestrictedControllerMessenger', () => { const messageHandler = sinon.stub(); const pingHandler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - messageHandler, - ); + restrictedMessenger.subscribe('MessageController:message', messageHandler); - restrictedControllerMessenger.subscribe( - 'MessageController:ping', - pingHandler, - ); + restrictedMessenger.subscribe('MessageController:ping', pingHandler); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); - restrictedControllerMessenger.publish('MessageController:ping'); + restrictedMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.publish('MessageController:ping'); expect(messageHandler.calledWithExactly('hello')).toBe(true); expect(messageHandler.callCount).toBe(1); @@ -789,16 +794,16 @@ describe('RestrictedControllerMessenger', () => { it('should publish event with no payload to subscriber', () => { type PingEvent = { type: 'PingController:ping'; payload: [] }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'PingController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe('PingController:ping', handler); - restrictedControllerMessenger.publish('PingController:ping'); + restrictedMessenger.subscribe('PingController:ping', handler); + restrictedMessenger.publish('PingController:ping'); expect(handler.calledWithExactly()).toBe(true); expect(handler.callCount).toBe(1); @@ -809,24 +814,17 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string, string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - restrictedControllerMessenger.publish( - 'MessageController:message', - 'hello', - 'there', - ); + restrictedMessenger.publish('MessageController:message', 'hello', 'there'); expect(handler.calledWithExactly('hello', 'there')).toBe(true); expect(handler.callCount).toBe(1); @@ -837,24 +835,18 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.subscribe('MessageController:message', handler); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -865,8 +857,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -874,16 +866,10 @@ describe('RestrictedControllerMessenger', () => { const handler1 = sinon.stub(); const handler2 = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler1, - ); + restrictedMessenger.subscribe('MessageController:message', handler1); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler2, - ); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.subscribe('MessageController:message', handler2); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler1.calledWithExactly('hello')).toBe(true); expect(handler1.callCount).toBe(1); @@ -896,24 +882,18 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - restrictedControllerMessenger.unsubscribe( - 'MessageController:message', - handler, - ); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.unsubscribe('MessageController:message', handler); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.callCount).toBe(0); }); @@ -923,8 +903,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -932,10 +912,7 @@ describe('RestrictedControllerMessenger', () => { const handler = sinon.stub(); expect(() => - restrictedControllerMessenger.unsubscribe( - 'MessageController:message', - handler, - ), + restrictedMessenger.unsubscribe('MessageController:message', handler), ).toThrow(`Subscription not found for event: MessageController:message`); }); @@ -944,8 +921,8 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], @@ -953,16 +930,10 @@ describe('RestrictedControllerMessenger', () => { const handler1 = sinon.stub(); const handler2 = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler1, - ); + restrictedMessenger.subscribe('MessageController:message', handler1); expect(() => - restrictedControllerMessenger.unsubscribe( - 'MessageController:message', - handler2, - ), + restrictedMessenger.unsubscribe('MessageController:message', handler2), ).toThrow(`Subscription not found for event: MessageController:message`); }); @@ -971,23 +942,18 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - restrictedControllerMessenger.clearEventSubscriptions( - 'MessageController:message', - ); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.clearEventSubscriptions('MessageController:message'); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.callCount).toBe(0); }); @@ -997,17 +963,15 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); expect(() => - restrictedControllerMessenger.clearEventSubscriptions( - 'MessageController:message', - ), + restrictedMessenger.clearEventSubscriptions('MessageController:message'), ).not.toThrow(); }); @@ -1016,21 +980,21 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); let count = 0; - restrictedControllerMessenger.registerActionHandler( + restrictedMessenger.registerActionHandler( 'CountController:count', (increment: number) => { count += increment; }, ); - restrictedControllerMessenger.call('CountController:count', 1); + restrictedMessenger.call('CountController:count', 1); expect(count).toBe(1); }); @@ -1040,14 +1004,13 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger(); - const externalRestrictedControllerMessenger = - controllerMessenger.getRestricted({ - name: 'CountController', - allowedActions: [], - allowedEvents: [], - }); - const restrictedControllerMessenger = controllerMessenger.getRestricted< + const messenger = new Messenger(); + const externalRestrictedMessenger = messenger.getRestricted({ + name: 'CountController', + allowedActions: [], + allowedEvents: [], + }); + const restrictedMessenger = messenger.getRestricted< 'OtherController', CountAction['type'] >({ @@ -1057,13 +1020,13 @@ describe('RestrictedControllerMessenger', () => { }); let count = 0; - externalRestrictedControllerMessenger.registerActionHandler( + externalRestrictedMessenger.registerActionHandler( 'CountController:count', (increment: number) => { count += increment; }, ); - restrictedControllerMessenger.call('CountController:count', 1); + restrictedMessenger.call('CountController:count', 1); expect(count).toBe(1); }); @@ -1073,20 +1036,17 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const restrictedControllerMessenger = controllerMessenger.getRestricted({ + const messenger = new Messenger(); + const restrictedMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: [], }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - restrictedControllerMessenger.publish('MessageController:message', 'hello'); + restrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -1097,14 +1057,13 @@ describe('RestrictedControllerMessenger', () => { type: 'MessageController:message'; payload: [string]; }; - const controllerMessenger = new ControllerMessenger(); - const externalRestrictedControllerMessenger = - controllerMessenger.getRestricted({ - name: 'MessageController', - allowedActions: [], - allowedEvents: [], - }); - const restrictedControllerMessenger = controllerMessenger.getRestricted< + const messenger = new Messenger(); + const externalRestrictedMessenger = messenger.getRestricted({ + name: 'MessageController', + allowedActions: [], + allowedEvents: [], + }); + const restrictedMessenger = messenger.getRestricted< 'OtherController', never, MessageEvent['type'] @@ -1115,15 +1074,9 @@ describe('RestrictedControllerMessenger', () => { }); const handler = sinon.stub(); - restrictedControllerMessenger.subscribe( - 'MessageController:message', - handler, - ); + restrictedMessenger.subscribe('MessageController:message', handler); - externalRestrictedControllerMessenger.publish( - 'MessageController:message', - 'hello', - ); + externalRestrictedMessenger.publish('MessageController:message', 'hello'); expect(handler.calledWithExactly('hello')).toBe(true); expect(handler.callCount).toBe(1); @@ -1140,24 +1093,21 @@ describe('RestrictedControllerMessenger', () => { type: 'CountController:count'; handler: (increment: number) => void; }; - const controllerMessenger = new ControllerMessenger< - MessageAction | CountAction, - never - >(); + const messenger = new Messenger(); - const messageControllerMessenger = controllerMessenger.getRestricted({ + const messageMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: ['CountController:count'], allowedEvents: [], }); - const countControllerMessenger = controllerMessenger.getRestricted({ + const countMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); let count = 0; - countControllerMessenger.registerActionHandler( + countMessenger.registerActionHandler( 'CountController:count', (increment: number) => { count += increment; @@ -1165,22 +1115,22 @@ describe('RestrictedControllerMessenger', () => { ); let fullMessage = ''; - messageControllerMessenger.registerActionHandler( + messageMessenger.registerActionHandler( 'MessageController:concat', (message: string) => { fullMessage += message; }, ); - messageControllerMessenger.registerActionHandler( + messageMessenger.registerActionHandler( 'MessageController:reset', (message: string) => { fullMessage = message; }, ); - messageControllerMessenger.call('MessageController:reset', 'hello'); - messageControllerMessenger.call('CountController:count', 1); + messageMessenger.call('MessageController:reset', 'hello'); + messageMessenger.call('CountController:count', 1); expect(fullMessage).toBe('hello'); expect(count).toBe(1); @@ -1191,35 +1141,29 @@ describe('RestrictedControllerMessenger', () => { | { type: 'MessageController:message'; payload: [string] } | { type: 'MessageController:ping'; payload: [] }; type CountEvent = { type: 'CountController:update'; payload: [number] }; - const controllerMessenger = new ControllerMessenger< - never, - MessageEvent | CountEvent - >(); + const messenger = new Messenger(); - const messageControllerMessenger = controllerMessenger.getRestricted({ + const messageMessenger = messenger.getRestricted({ name: 'MessageController', allowedActions: [], allowedEvents: ['CountController:update'], }); - const countControllerMessenger = controllerMessenger.getRestricted({ + const countMessenger = messenger.getRestricted({ name: 'CountController', allowedActions: [], allowedEvents: [], }); let pings = 0; - messageControllerMessenger.subscribe('MessageController:ping', () => { + messageMessenger.subscribe('MessageController:ping', () => { pings += 1; }); let currentCount; - messageControllerMessenger.subscribe( - 'CountController:update', - (newCount: number) => { - currentCount = newCount; - }, - ); - messageControllerMessenger.publish('MessageController:ping'); - countControllerMessenger.publish('CountController:update', 10); + messageMessenger.subscribe('CountController:update', (newCount: number) => { + currentCount = newCount; + }); + messageMessenger.publish('MessageController:ping'); + countMessenger.publish('CountController:update', 10); expect(pings).toBe(1); expect(currentCount).toBe(10); diff --git a/packages/base-controller/src/RestrictedControllerMessenger.ts b/packages/base-controller/src/RestrictedMessenger.ts similarity index 80% rename from packages/base-controller/src/RestrictedControllerMessenger.ts rename to packages/base-controller/src/RestrictedMessenger.ts index 62c8ae0f8e..c1cd62b6ad 100644 --- a/packages/base-controller/src/RestrictedControllerMessenger.ts +++ b/packages/base-controller/src/RestrictedMessenger.ts @@ -1,7 +1,7 @@ import type { ActionConstraint, ActionHandler, - ControllerMessenger, + Messenger, EventConstraint, ExtractActionParameters, ExtractActionResponse, @@ -11,29 +11,40 @@ import type { NotNamespacedBy, SelectorEventHandler, SelectorFunction, -} from './ControllerMessenger'; +} from './Messenger'; /** - * A universal supertype of all `RestrictedControllerMessenger` instances. - * This type can be assigned to any `RestrictedControllerMessenger` type. + * A universal supertype of all `RestrictedMessenger` instances. This type can be assigned to any + * `RestrictedMessenger` type. * - * @template ControllerName - Name of the controller. Optionally can be used to - * narrow this type to a constraint for the messenger of a specific controller. + * @template Namespace - Name of the module this messenger is for. Optionally can be used to + * narrow this type to a constraint for the messenger of a specific module. + */ +export type RestrictedMessengerConstraint = + RestrictedMessenger< + Namespace, + ActionConstraint, + EventConstraint, + string, + string + >; + +/** + * A universal supertype of all `RestrictedMessenger` instances. This type can be assigned to any + * `RestrictedMessenger` type. + * + * @template Namespace - Name of the module this messenger is for. Optionally can be used to + * narrow this type to a constraint for the messenger of a specific module. + * @deprecated This has been renamed to `RestrictedMessengerConstraint`. */ export type RestrictedControllerMessengerConstraint< - ControllerName extends string = string, -> = RestrictedControllerMessenger< - ControllerName, - ActionConstraint, - EventConstraint, - string, - string ->; + Namespace extends string = string, +> = RestrictedMessengerConstraint; /** - * A restricted controller messenger. + * A restricted messenger. * - * This acts as a wrapper around the controller messenger instance that restricts access to actions + * This acts as a wrapper around the messenger instance that restricts access to actions * and events. * * @template Namespace - The namespace for this messenger. Typically this is the name of the controller or @@ -46,55 +57,64 @@ export type RestrictedControllerMessengerConstraint< * @template AllowedEvent - A type union of the 'type' string for any allowed events. * This must not include internal events that are in the messenger's namespace. */ -export class RestrictedControllerMessenger< +export class RestrictedMessenger< Namespace extends string, Action extends ActionConstraint, Event extends EventConstraint, AllowedAction extends string, AllowedEvent extends string, > { - readonly #controllerMessenger: ControllerMessenger< - ActionConstraint, - EventConstraint - >; + readonly #messenger: Messenger; - readonly #controllerName: Namespace; + readonly #namespace: Namespace; readonly #allowedActions: NotNamespacedBy[]; readonly #allowedEvents: NotNamespacedBy[]; /** - * Constructs a restricted controller messenger + * Constructs a restricted messenger * * The provided allowlists grant the ability to call the listed actions and subscribe to the * listed events. The "name" provided grants ownership of any actions and events under that * namespace. Ownership allows registering actions and publishing events, as well as * unregistering actions and clearing event subscriptions. * - * @param options - The controller options. - * @param options.controllerMessenger - The controller messenger instance that is being wrapped. + * @param options - Options. + * @param options.controllerMessenger - The messenger instance that is being wrapped. (deprecated) + * @param options.messenger - The messenger instance that is being wrapped. * @param options.name - The name of the thing this messenger will be handed to (e.g. the * controller name). This grants "ownership" of actions and events under this namespace to the - * restricted controller messenger returned. - * @param options.allowedActions - The list of actions that this restricted controller messenger - * should be alowed to call. - * @param options.allowedEvents - The list of events that this restricted controller messenger - * should be allowed to subscribe to. + * restricted messenger returned. + * @param options.allowedActions - The list of actions that this restricted messenger should be + * allowed to call. + * @param options.allowedEvents - The list of events that this restricted messenger should be + * allowed to subscribe to. */ constructor({ controllerMessenger, + messenger, name, allowedActions, allowedEvents, }: { - controllerMessenger: ControllerMessenger; + controllerMessenger?: Messenger; + messenger?: Messenger; name: Namespace; allowedActions: NotNamespacedBy[]; allowedEvents: NotNamespacedBy[]; }) { - this.#controllerMessenger = controllerMessenger; - this.#controllerName = name; + if (messenger && controllerMessenger) { + throw new Error( + `Both messenger properties provided. Provide message using only 'messenger' option, 'controllerMessenger' is deprecated`, + ); + } else if (!messenger && !controllerMessenger) { + throw new Error('Messenger not provided'); + } + // The above condition guarantees that one of these options is defined. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.#messenger = (messenger ?? controllerMessenger)!; + this.#namespace = name; this.#allowedActions = allowedActions; this.#allowedEvents = allowedEvents; } @@ -119,11 +139,11 @@ export class RestrictedControllerMessenger< if (!this.#isInCurrentNamespace(action)) { throw new Error( `Only allowed registering action handlers prefixed by '${ - this.#controllerName + this.#namespace }:'`, ); } - this.#controllerMessenger.registerActionHandler(action, handler); + this.#messenger.registerActionHandler(action, handler); } /** @@ -144,11 +164,11 @@ export class RestrictedControllerMessenger< if (!this.#isInCurrentNamespace(action)) { throw new Error( `Only allowed unregistering action handlers prefixed by '${ - this.#controllerName + this.#namespace }:'`, ); } - this.#controllerMessenger.unregisterActionHandler(action); + this.#messenger.unregisterActionHandler(action); } /** @@ -177,10 +197,7 @@ export class RestrictedControllerMessenger< if (!this.#isAllowedAction(actionType)) { throw new Error(`Action missing from allow list: ${actionType}`); } - const response = this.#controllerMessenger.call( - actionType, - ...params, - ); + const response = this.#messenger.call(actionType, ...params); return response; } @@ -210,10 +227,10 @@ export class RestrictedControllerMessenger< /* istanbul ignore if */ // Branch unreachable with valid types if (!this.#isInCurrentNamespace(eventType)) { throw new Error( - `Only allowed publishing events prefixed by '${this.#controllerName}:'`, + `Only allowed publishing events prefixed by '${this.#namespace}:'`, ); } - this.#controllerMessenger.registerInitialEventPayload({ + this.#messenger.registerInitialEventPayload({ eventType, getPayload, }); @@ -239,10 +256,10 @@ export class RestrictedControllerMessenger< /* istanbul ignore if */ // Branch unreachable with valid types if (!this.#isInCurrentNamespace(event)) { throw new Error( - `Only allowed publishing events prefixed by '${this.#controllerName}:'`, + `Only allowed publishing events prefixed by '${this.#namespace}:'`, ); } - this.#controllerMessenger.publish(event, ...payload); + this.#messenger.publish(event, ...payload); } /** @@ -255,7 +272,7 @@ export class RestrictedControllerMessenger< * @param eventType - The event type. This is a unique identifier for this event. * @param handler - The event handler. The type of the parameters for this event handler must * match the type of the payload for this event type. - * @throws Will throw if the given event is not an allowed event for this controller messenger. + * @throws Will throw if the given event is not an allowed event for this messenger. * @template EventType - A type union of Event type strings. */ subscribe< @@ -280,7 +297,7 @@ export class RestrictedControllerMessenger< * @param selector - The selector function used to select relevant data from * the event payload. The type of the parameters for this selector must match * the type of the payload for this event type. - * @throws Will throw if the given event is not an allowed event for this controller messenger. + * @throws Will throw if the given event is not an allowed event for this messenger. * @template EventType - A type union of Event type strings. * @template SelectorReturnValue - The selector return value. */ @@ -310,9 +327,9 @@ export class RestrictedControllerMessenger< } if (selector) { - return this.#controllerMessenger.subscribe(event, handler, selector); + return this.#messenger.subscribe(event, handler, selector); } - return this.#controllerMessenger.subscribe(event, handler); + return this.#messenger.subscribe(event, handler); } /** @@ -324,7 +341,7 @@ export class RestrictedControllerMessenger< * * @param event - The event type. This is a unique identifier for this event. * @param handler - The event handler to unregister. - * @throws Will throw if the given event is not an allowed event for this controller messenger. + * @throws Will throw if the given event is not an allowed event for this messenger. * @template EventType - A type union of allowed Event type strings. */ unsubscribe< @@ -335,7 +352,7 @@ export class RestrictedControllerMessenger< if (!this.#isAllowedEvent(event)) { throw new Error(`Event missing from allow list: ${event}`); } - this.#controllerMessenger.unsubscribe(event, handler); + this.#messenger.unsubscribe(event, handler); } /** @@ -354,10 +371,10 @@ export class RestrictedControllerMessenger< >(event: EventType) { if (!this.#isInCurrentNamespace(event)) { throw new Error( - `Only allowed clearing events prefixed by '${this.#controllerName}:'`, + `Only allowed clearing events prefixed by '${this.#namespace}:'`, ); } - this.#controllerMessenger.clearEventSubscriptions(event); + this.#messenger.clearEventSubscriptions(event); } /** @@ -409,6 +426,8 @@ export class RestrictedControllerMessenger< * @returns Whether the name is within the current namespace */ #isInCurrentNamespace(name: string): name is NamespacedName { - return name.startsWith(`${this.#controllerName}:`); + return name.startsWith(`${this.#namespace}:`); } } + +export { RestrictedMessenger as RestrictedControllerMessenger }; diff --git a/packages/base-controller/src/index.ts b/packages/base-controller/src/index.ts index a4eab5b572..af19ddfc50 100644 --- a/packages/base-controller/src/index.ts +++ b/packages/base-controller/src/index.ts @@ -41,7 +41,13 @@ export type { NamespacedBy, NotNamespacedBy, NamespacedName, -} from './ControllerMessenger'; -export { ControllerMessenger } from './ControllerMessenger'; -export type { RestrictedControllerMessengerConstraint } from './RestrictedControllerMessenger'; -export { RestrictedControllerMessenger } from './RestrictedControllerMessenger'; +} from './Messenger'; +export { ControllerMessenger, Messenger } from './Messenger'; +export type { + RestrictedControllerMessengerConstraint, + RestrictedMessengerConstraint, +} from './RestrictedMessenger'; +export { + RestrictedControllerMessenger, + RestrictedMessenger, +} from './RestrictedMessenger'; From 57ef061a8eaf202df4b8fc3c59682a4761dacf69 Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Mon, 16 Dec 2024 16:31:13 +0000 Subject: [PATCH 19/23] fix: check push notification compatibility before setting up firebase (#5069) ## Explanation Before we were initialising firebase without checking if the browser has correct setup for it. Thus causing errors logs to be made. (not breaking the app, but still producing errors). We now do a check to ensure we can initialise firebase. ## References https://github.com/MetaMask/metamask-extension/issues/26173 ## Changelog ### `@metamask/notification-services-controller` - **ADDED**: `getPushAvailability` method to check if we can use push notifications. - **CHANGED**: `getFirebaseMessaging()` now optionally returns null. This is an internal method not exposed outside. - **CHANGED**: `unsubscribePushNotifications` is not optional. This is internal and not exposed outside module. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../services/push/push-web.test.ts | 377 ++++++++++++++++++ .../services/push/push-web.ts | 36 +- .../services/services.ts | 2 +- 3 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.test.ts diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.test.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.test.ts new file mode 100644 index 0000000000..0ffd235dc2 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.test.ts @@ -0,0 +1,377 @@ +import * as FirebaseAppModule from 'firebase/app'; +import * as FirebaseMessagingModule from 'firebase/messaging'; +import * as FirebaseMessagingSWModule from 'firebase/messaging/sw'; +import log from 'loglevel'; + +import { processNotification } from '../../../NotificationServicesController'; +import { createMockNotificationEthSent } from '../../../NotificationServicesController/__fixtures__'; +import * as PushWebModule from './push-web'; +import { + createRegToken, + deleteRegToken, + listenToPushNotificationsReceived, + listenToPushNotificationsClicked, +} from './push-web'; + +jest.mock('firebase/app'); +jest.mock('firebase/messaging'); +jest.mock('firebase/messaging/sw'); + +const mockEnv = { + apiKey: 'test-apiKey', + authDomain: 'test-authDomain', + storageBucket: 'test-storageBucket', + projectId: 'test-projectId', + messagingSenderId: 'test-messagingSenderId', + appId: 'test-appId', + measurementId: 'test-measurementId', + vapidKey: 'test-vapidKey', +}; + +const firebaseApp: FirebaseAppModule.FirebaseApp = { + name: '', + automaticDataCollectionEnabled: false, + options: mockEnv, +}; + +const arrangeFirebaseAppMocks = () => { + const mockGetApp = jest + .spyOn(FirebaseAppModule, 'getApp') + .mockReturnValue(firebaseApp); + + const mockInitializeApp = jest + .spyOn(FirebaseAppModule, 'initializeApp') + .mockReturnValue(firebaseApp); + + return { mockGetApp, mockInitializeApp }; +}; + +const arrangeFirebaseMessagingSWMocks = () => { + const mockIsSupported = jest + .spyOn(FirebaseMessagingSWModule, 'isSupported') + .mockResolvedValue(true); + + const getMessaging = jest + .spyOn(FirebaseMessagingSWModule, 'getMessaging') + .mockReturnValue({ app: firebaseApp }); + + const mockOnBackgroundMessageUnsub = jest.fn(); + const mockOnBackgroundMessage = jest + .spyOn(FirebaseMessagingSWModule, 'onBackgroundMessage') + .mockReturnValue(mockOnBackgroundMessageUnsub); + + return { + mockIsSupported, + getMessaging, + mockOnBackgroundMessage, + mockOnBackgroundMessageUnsub, + }; +}; + +const arrangeFirebaseMessagingMocks = () => { + const mockGetToken = jest + .spyOn(FirebaseMessagingModule, 'getToken') + .mockResolvedValue('test-token'); + + const mockDeleteToken = jest + .spyOn(FirebaseMessagingModule, 'deleteToken') + .mockResolvedValue(true); + + return { mockGetToken, mockDeleteToken }; +}; + +describe('createRegToken() tests', () => { + const TEST_TOKEN = 'test-token'; + + const arrange = () => { + const firebaseMocks = { + ...arrangeFirebaseAppMocks(), + ...arrangeFirebaseMessagingSWMocks(), + ...arrangeFirebaseMessagingMocks(), + }; + + firebaseMocks.mockGetToken.mockResolvedValue(TEST_TOKEN); + + return { + ...firebaseMocks, + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + + // TODO - replace with jest.replaceProperty once we upgrade jest. + Object.defineProperty(PushWebModule, 'supportedCache', { value: null }); + }); + + it('should return a registration token when Firebase is supported', async () => { + const { mockGetApp, mockGetToken } = arrange(); + + const token = await createRegToken(mockEnv); + + expect(mockGetApp).toHaveBeenCalled(); + expect(mockGetToken).toHaveBeenCalled(); + expect(token).toBe(TEST_TOKEN); + }); + + it('should return null when Firebase is not supported', async () => { + const { mockIsSupported } = arrange(); + mockIsSupported.mockResolvedValueOnce(false); + + const token = await createRegToken(mockEnv); + + expect(token).toBeNull(); + }); + + it('should return null if an error occurs', async () => { + const { mockGetToken } = arrange(); + mockGetToken.mockRejectedValueOnce(new Error('Error getting token')); + + const token = await createRegToken(mockEnv); + + expect(token).toBeNull(); + }); + + it('should initialize firebase if has not been created yet', async () => { + const { mockGetApp, mockInitializeApp, mockGetToken } = arrange(); + mockGetApp.mockImplementation(() => { + throw new Error('mock Firebase GetApp failure'); + }); + + const token = await createRegToken(mockEnv); + + expect(mockGetApp).toHaveBeenCalled(); + expect(mockInitializeApp).toHaveBeenCalled(); + expect(mockGetToken).toHaveBeenCalled(); + expect(token).toBe(TEST_TOKEN); + }); +}); + +describe('deleteRegToken() tests', () => { + const arrange = () => { + return { + ...arrangeFirebaseAppMocks(), + ...arrangeFirebaseMessagingSWMocks(), + ...arrangeFirebaseMessagingMocks(), + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + + // TODO - replace with jest.replaceProperty once we upgrade jest. + Object.defineProperty(PushWebModule, 'supportedCache', { value: null }); + }); + + it('should return true when the token is successfully deleted', async () => { + const { mockGetApp, mockDeleteToken } = arrange(); + + const result = await deleteRegToken(mockEnv); + + expect(mockGetApp).toHaveBeenCalled(); + expect(mockDeleteToken).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should return true when Firebase is not supported', async () => { + const { mockIsSupported, mockDeleteToken } = arrange(); + mockIsSupported.mockResolvedValueOnce(false); + + const result = await deleteRegToken(mockEnv); + + expect(result).toBe(true); + expect(mockDeleteToken).not.toHaveBeenCalled(); + }); + + it('should return false if an error occurs', async () => { + const { mockDeleteToken } = arrange(); + mockDeleteToken.mockRejectedValueOnce(new Error('Error deleting token')); + + const result = await deleteRegToken(mockEnv); + + expect(result).toBe(false); + }); +}); + +describe('listenToPushNotificationsReceived() tests', () => { + const arrange = () => { + return { + ...arrangeFirebaseAppMocks(), + ...arrangeFirebaseMessagingSWMocks(), + ...arrangeFirebaseMessagingMocks(), + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + + // TODO - replace with jest.replaceProperty once we upgrade jest. + Object.defineProperty(PushWebModule, 'supportedCache', { value: null }); + }); + + it('should return an unsubscribe function when Firebase is supported', async () => { + const { mockGetApp, mockOnBackgroundMessage } = arrange(); + + const handler = jest.fn(); + const unsubscribe = await listenToPushNotificationsReceived( + mockEnv, + handler, + ); + + expect(mockGetApp).toHaveBeenCalled(); + expect(mockOnBackgroundMessage).toHaveBeenCalled(); + expect(unsubscribe).not.toBeNull(); + }); + + it('should return null when Firebase is not supported', async () => { + const { mockIsSupported } = arrange(); + mockIsSupported.mockResolvedValueOnce(false); + + const handler = jest.fn(); + const unsubscribe = await listenToPushNotificationsReceived( + mockEnv, + handler, + ); + + expect(unsubscribe).toBeNull(); + }); + + it('should be able to unsubscribe when invoked', async () => { + const { mockOnBackgroundMessageUnsub } = arrange(); + + const handler = jest.fn(); + const unsubscribe = await listenToPushNotificationsReceived( + mockEnv, + handler, + ); + + expect(unsubscribe).not.toBeNull(); + unsubscribe?.(); + expect(mockOnBackgroundMessageUnsub).toHaveBeenCalled(); + }); + + describe('handler tests', () => { + const arrangeTest = async () => { + const { mockOnBackgroundMessage } = arrange(); + + const handler = jest.fn(); + await listenToPushNotificationsReceived(mockEnv, handler); + + // Simulate receiving a background message + const invokeBackgroundMessage = mockOnBackgroundMessage.mock + .calls[0][1] as FirebaseMessagingModule.NextFn; + + return { + handler, + invokeBackgroundMessage, + }; + }; + + const arrangeActInvokeBackgroundMessage = async (testData: unknown) => { + const { handler, invokeBackgroundMessage } = await arrangeTest(); + + const payload = { + data: { + data: testData, + }, + } as unknown as FirebaseMessagingSWModule.MessagePayload; + + invokeBackgroundMessage(payload); + + return { handler }; + }; + + it('should call the handler with the processed notification', async () => { + const { handler } = await arrangeActInvokeBackgroundMessage( + JSON.stringify(createMockNotificationEthSent()), + ); + expect(handler).toHaveBeenCalled(); + }); + + it('should return early without calling handler if no data in background message', async () => { + const { handler } = await arrangeActInvokeBackgroundMessage( + JSON.stringify(undefined), + ); + expect(handler).not.toHaveBeenCalled(); + }); + + it('should error if unable to process and send a push notification', async () => { + const { handler, invokeBackgroundMessage } = await arrangeTest(); + jest.spyOn(log, 'error').mockImplementation(jest.fn()); + + const payload = { + data: { + data: JSON.stringify({ badNotification: 'bad' }), + }, + } as unknown as FirebaseMessagingSWModule.MessagePayload; + + await expect(invokeBackgroundMessage(payload)).rejects.toThrow( + expect.any(Error), + ); + + expect(handler).not.toHaveBeenCalled(); + }); + }); +}); + +describe('listenToPushNotificationsClicked() tests', () => { + const arrange = () => { + const mockHandler = jest.fn(); + return { mockHandler }; + }; + + const arrangeTest = () => { + const { mockHandler } = arrange(); + + const unsubscribe = listenToPushNotificationsClicked(mockHandler); + + const notificationData = processNotification( + createMockNotificationEthSent(), + ); + + const mockNotificationEvent = new Event( + 'notificationclick', + ) as NotificationEvent; + Object.assign(mockNotificationEvent, { + notification: { data: notificationData }, + }); + + return { + mockHandler, + unsubscribe, + notificationData, + mockNotificationEvent, + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call the handler with the notification event and data when a notification is clicked', () => { + const { + mockHandler, + unsubscribe, + notificationData, + mockNotificationEvent, + } = arrangeTest(); + + self.dispatchEvent(mockNotificationEvent); + + expect(mockHandler).toHaveBeenCalledWith( + mockNotificationEvent, + notificationData, + ); + + unsubscribe(); + }); + + it('should remove the event listener when unsubscribe is called', () => { + const { mockHandler, unsubscribe, mockNotificationEvent } = arrangeTest(); + + unsubscribe(); + + self.dispatchEvent(mockNotificationEvent); + expect(mockHandler).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts index f89d69da2d..ccfab4e40f 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/push/push-web.ts @@ -4,7 +4,11 @@ import type { FirebaseApp } from 'firebase/app'; import { getApp, initializeApp } from 'firebase/app'; import { getToken, deleteToken } from 'firebase/messaging'; -import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw'; +import { + getMessaging, + onBackgroundMessage, + isSupported, +} from 'firebase/messaging/sw'; import type { Messaging, MessagePayload } from 'firebase/messaging/sw'; import log from 'loglevel'; @@ -15,6 +19,15 @@ import type { PushNotificationEnv } from '../../types/firebase'; declare const self: ServiceWorkerGlobalScope; +// Exported to help testing +// eslint-disable-next-line import/no-mutable-exports +export let supportedCache: boolean | null = null; + +const getPushAvailability = async () => { + supportedCache ??= await isSupported(); + return supportedCache; +}; + const createFirebaseApp = async ( env: PushNotificationEnv, ): Promise => { @@ -36,7 +49,12 @@ const createFirebaseApp = async ( const getFirebaseMessaging = async ( env: PushNotificationEnv, -): Promise => { +): Promise => { + const supported = await getPushAvailability(); + if (!supported) { + return null; + } + const app = await createFirebaseApp(env); return getMessaging(app); }; @@ -52,6 +70,10 @@ export async function createRegToken( ): Promise { try { const messaging = await getFirebaseMessaging(env); + if (!messaging) { + return null; + } + const token = await getToken(messaging, { serviceWorkerRegistration: self.registration, vapidKey: env.vapidKey, @@ -73,6 +95,10 @@ export async function deleteRegToken( ): Promise { try { const messaging = await getFirebaseMessaging(env); + if (!messaging) { + return true; + } + await deleteToken(messaging); return true; } catch (error) { @@ -89,8 +115,12 @@ export async function deleteRegToken( export async function listenToPushNotificationsReceived( env: PushNotificationEnv, handler: (notification: Types.INotification) => void | Promise, -) { +): Promise<(() => void) | null> { const messaging = await getFirebaseMessaging(env); + if (!messaging) { + return null; + } + const unsubscribePushNotifications = onBackgroundMessage( messaging, // eslint-disable-next-line @typescript-eslint/no-misused-promises diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts index 1bb84b14a5..df07e6acef 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/services/services.ts @@ -288,7 +288,7 @@ export async function listenToPushNotifications( listenToPushNotificationsClicked(listenToPushClicked); const unsubscribe = () => { - unsubscribePushNotifications(); + unsubscribePushNotifications?.(); unsubscribeNotificationClicks(); }; From 3ba11472793bce8e1b081be2f60cbcd75a394f08 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 16 Dec 2024 09:24:22 -0800 Subject: [PATCH 20/23] feat: Add `caip25CaveatBuilder` to `@metamask/multichain` (#5064) ## Explanation Moves the validator from the `caip25EndowmentBuilder` into a new `caip25CaveatBuilder` helper to fix the incorrect full validation of the CAIP-25 caveat inside the CAIP-25 permission. ## References See: https://github.com/MetaMask/metamask-extension/pull/27847#discussion_r1881175092 Extension: https://github.com/MetaMask/metamask-extension/pull/29166 ## Changelog ### `@metamask/multichain` - **BREAKING**: The validator returned by ` caip25EndowmentBuilder` now only verifies that there is single CAIP-25 caveat and nothing else. - **ADDED**: Adds `caip25CaveatBuilder` helper that builds a specification for the CAIP-25 caveat that can be passed to the relevant `PermissionController` constructor param. ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Alex Donesky Co-authored-by: Mark Stacey --- .../multichain/src/caip25Permission.test.ts | 491 ++++++++---------- packages/multichain/src/caip25Permission.ts | 126 +++-- packages/multichain/src/index.test.ts | 1 + packages/multichain/src/index.ts | 1 + 4 files changed, 286 insertions(+), 333 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index b416f3fab6..b4caba7456 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -10,6 +10,7 @@ import { Caip25EndowmentPermissionName, Caip25CaveatMutators, createCaip25Caveat, + caip25CaveatBuilder, } from './caip25Permission'; import * as ScopeSupported from './scope/supported'; @@ -394,14 +395,7 @@ describe('caip25EndowmentBuilder', () => { }); describe('permission validator', () => { - const findNetworkClientIdByChainId = jest.fn(); - const listAccounts = jest.fn(); - const { validator } = caip25EndowmentBuilder.specificationBuilder({ - methodHooks: { - findNetworkClientIdByChainId, - listAccounts, - }, - }); + const { validator } = caip25EndowmentBuilder.specificationBuilder({}); it('throws an error if there is not exactly one caveat', () => { expect(() => { @@ -463,296 +457,233 @@ describe('caip25EndowmentBuilder', () => { ), ); }); + }); +}); - it('throws an error if the CAIP-25 caveat is malformed', () => { - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - missingRequiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); +describe('caip25CaveatBuilder', () => { + const findNetworkClientIdByChainId = jest.fn(); + const listAccounts = jest.fn(); + const { validator } = caip25CaveatBuilder({ + findNetworkClientIdByChainId, + listAccounts, + }); - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - missingOptionalScopes: {}, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); + it('throws an error if the CAIP-25 caveat is malformed', () => { + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + missingRequiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + missingOptionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: 'NotABoolean', + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + }); - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: 'NotABoolean', - }, + it('asserts the internal required scopeStrings are supported', () => { + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); - }); - - it('asserts the internal required scopeStrings are supported', () => { - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:1', - expect.any(Function), - ); - - MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); - }); + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } + expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( + 'eip155:1', + expect.any(Function), + ); + + MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + }); - it('asserts the internal optional scopeStrings are supported', () => { - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + it('asserts the internal optional scopeStrings are supported', () => { + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } - - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:5', - expect.any(Function), - ); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } - MockScopeSupported.isSupportedScopeString.mock.calls[1][1]('0x5'); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x5'); - }); + expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( + 'eip155:5', + expect.any(Function), + ); - it('does not throw if unable to find a network client for the chainId', () => { - findNetworkClientIdByChainId.mockImplementation(() => { - throw new Error('unable to find network client'); - }); - try { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - } catch (err) { - // noop - } + MockScopeSupported.isSupportedScopeString.mock.calls[1][1]('0x5'); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x5'); + }); - expect( - MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'), - ).toBe(false); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + it('does not throw if unable to find a network client for the chainId', () => { + findNetworkClientIdByChainId.mockImplementation(() => { + throw new Error('unable to find network client'); }); + try { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + } catch (err) { + // noop + } + + expect( + MockScopeSupported.isSupportedScopeString.mock.calls[0][1]('0x1'), + ).toBe(false); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + }); - it('throws if not all scopeStrings are supported', () => { - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + it('throws if not all scopeStrings are supported', () => { + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ), - ); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, + ), + ); + }); - it('throws if the eth accounts specified in the internal scopeObjects are not found in the wallet keyring', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - listAccounts.mockReturnValue([{ address: '0xdead' }]); // missing '0xbeef' + it('throws if the eth accounts specified in the internal scopeObjects are not found in the wallet keyring', () => { + MockScopeSupported.isSupportedScopeString.mockReturnValue(true); + listAccounts.mockReturnValue([{ address: '0xdead' }]); // missing '0xbeef' - expect(() => { - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + expect(() => { + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, - ), - ); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, + ), + ); + }); - it('does not throw if the CAIP-25 caveat value is valid', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - listAccounts.mockReturnValue([ - { address: '0xdead' }, - { address: '0xbeef' }, - ]); + it('does not throw if the CAIP-25 caveat value is valid', () => { + MockScopeSupported.isSupportedScopeString.mockReturnValue(true); + listAccounts.mockReturnValue([ + { address: '0xdead' }, + { address: '0xbeef' }, + ]); - expect( - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, + expect( + validator({ + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdead'], }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }), - ).toBeUndefined(); - }); + }, + optionalScopes: { + 'eip155:5': { + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }), + ).toBeUndefined(); }); }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 6bc47b8dbc..a865aefeb8 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -5,6 +5,7 @@ import type { ValidPermissionSpecification, PermissionValidatorConstraint, PermissionConstraint, + EndowmentCaveatSpecificationConstraint, } from '@metamask/permission-controller'; import { CaveatMutatorOperation, @@ -47,6 +48,11 @@ export type Caip25CaveatValue = { */ export const Caip25CaveatType = 'authorizedScopes'; +/** + * The target name of the CAIP-25 endowment permission. + */ +export const Caip25EndowmentPermissionName = 'endowment:caip25'; + /** * Creates a CAIP-25 permission caveat. * @param value - The CAIP-25 permission caveat value. @@ -59,75 +65,52 @@ export const createCaip25Caveat = (value: Caip25CaveatValue) => { }; }; -/** - * The target name of the CAIP-25 endowment permission. - */ -export const Caip25EndowmentPermissionName = 'endowment:caip25'; - -type Caip25EndowmentSpecification = ValidPermissionSpecification<{ - permissionType: PermissionType.Endowment; - targetName: typeof Caip25EndowmentPermissionName; - endowmentGetter: (_options?: EndowmentGetterParams) => null; - validator: PermissionValidatorConstraint; - allowedCaveats: Readonly> | null; -}>; - -type Caip25EndowmentSpecificationBuilderOptions = { - methodHooks: { - findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; - listAccounts: () => { address: Hex }[]; - }; +type Caip25EndowmentCaveatSpecificationBuilderOptions = { + findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; + listAccounts: () => { address: Hex }[]; }; /** - * Helper that returns a `endowment:caip25` specification that - * can be passed into the PermissionController constructor. + * Helper that returns a `authorizedScopes` CAIP-25 caveat specification + * that can be passed into the PermissionController constructor. * - * @param builderOptions - The specification builder options. - * @param builderOptions.methodHooks - The RPC method hooks needed by the method implementation. - * @returns The specification for the `caip25` endowment. + * @param options - The specification builder options. + * @param options.findNetworkClientIdByChainId - The hook for getting the networkClientId that serves a chainId. + * @param options.listAccounts - The hook for getting internalAccount objects for all evm accounts. + * @returns The specification for the `caip25` caveat. */ -const specificationBuilder: PermissionSpecificationBuilder< - PermissionType.Endowment, - Caip25EndowmentSpecificationBuilderOptions, - Caip25EndowmentSpecification -> = ({ methodHooks }: Caip25EndowmentSpecificationBuilderOptions) => { +export const caip25CaveatBuilder = ({ + findNetworkClientIdByChainId, + listAccounts, +}: Caip25EndowmentCaveatSpecificationBuilderOptions): EndowmentCaveatSpecificationConstraint & + Required> => { return { - permissionType: PermissionType.Endowment, - targetName: Caip25EndowmentPermissionName, - allowedCaveats: [Caip25CaveatType], - endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, - validator: (permission: PermissionConstraint) => { - const caip25Caveat = permission.caveats?.[0]; - if ( - permission.caveats?.length !== 1 || - caip25Caveat?.type !== Caip25CaveatType - ) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ); - } - + type: Caip25CaveatType, + validator: ( + caveat: { type: typeof Caip25CaveatType; value: unknown }, + _origin?: string, + _target?: string, + ) => { if ( - !caip25Caveat.value || - !hasProperty(caip25Caveat.value, 'requiredScopes') || - !hasProperty(caip25Caveat.value, 'optionalScopes') || - !hasProperty(caip25Caveat.value, 'isMultichainOrigin') || - typeof caip25Caveat.value.isMultichainOrigin !== 'boolean' + !caveat.value || + !hasProperty(caveat.value, 'requiredScopes') || + !hasProperty(caveat.value, 'optionalScopes') || + !hasProperty(caveat.value, 'isMultichainOrigin') || + typeof caveat.value.isMultichainOrigin !== 'boolean' ) { throw new Error( `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, ); } - const { requiredScopes, optionalScopes } = caip25Caveat.value; + const { requiredScopes, optionalScopes } = caveat.value; assertIsInternalScopesObject(requiredScopes); assertIsInternalScopesObject(optionalScopes); const isChainIdSupported = (chainId: Hex) => { try { - methodHooks.findNetworkClientIdByChainId(chainId); + findNetworkClientIdByChainId(chainId); return true; } catch (err) { return false; @@ -150,9 +133,9 @@ const specificationBuilder: PermissionSpecificationBuilder< // Fetch EVM accounts from native wallet keyring // These addresses are lowercased already - const existingEvmAddresses = methodHooks - .listAccounts() - .map((account) => account.address); + const existingEvmAddresses = listAccounts().map( + (account) => account.address, + ); const ethAccounts = getEthAccounts({ requiredScopes, optionalScopes, @@ -170,6 +153,43 @@ const specificationBuilder: PermissionSpecificationBuilder< }; }; +type Caip25EndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof Caip25EndowmentPermissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + validator: PermissionValidatorConstraint; + allowedCaveats: Readonly> | null; +}>; + +/** + * Helper that returns a `endowment:caip25` specification that + * can be passed into the PermissionController constructor. + * + * @returns The specification for the `caip25` endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + Record, + Caip25EndowmentSpecification +> = () => { + return { + permissionType: PermissionType.Endowment, + targetName: Caip25EndowmentPermissionName, + allowedCaveats: [Caip25CaveatType], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + validator: (permission: PermissionConstraint) => { + if ( + permission.caveats?.length !== 1 || + permission.caveats?.[0]?.type !== Caip25CaveatType + ) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, + ); + } + }, + }; +}; + /** * The `caip25` endowment specification builder. Passed to the * `PermissionController` for constructing and validating the diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 61f0fdcc42..38ab1bdde8 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -20,6 +20,7 @@ describe('@metamask/multichain', () => { "mergeScopeObject", "mergeScopes", "normalizeAndMergeScopes", + "caip25CaveatBuilder", "Caip25CaveatType", "createCaip25Caveat", "Caip25EndowmentPermissionName", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index d322c2b74d..5b56923ffa 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -39,6 +39,7 @@ export { export type { Caip25CaveatValue } from './caip25Permission'; export { + caip25CaveatBuilder, Caip25CaveatType, createCaip25Caveat, Caip25EndowmentPermissionName, From ba4dab8fab8a61bb72b605d41290916e9d2becd0 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 16 Dec 2024 12:40:59 -0800 Subject: [PATCH 21/23] Release/272.0.0 (#5070) ## Explanation Releases a major for the `@metamask/multichain` package --- package.json | 2 +- packages/multichain/CHANGELOG.md | 13 ++++++++++++- packages/multichain/package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e1b7daf616..4a6e195244 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "271.0.0", + "version": "272.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/multichain/CHANGELOG.md b/packages/multichain/CHANGELOG.md index 68060a640e..bc16f9ba8e 100644 --- a/packages/multichain/CHANGELOG.md +++ b/packages/multichain/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] + +### Added + +- Adds `caip25CaveatBuilder` helper that builds a specification for the CAIP-25 caveat that can be passed to the relevant `PermissionController` constructor param([#5064](https://github.com/MetaMask/core/pull/5064)). + +### Changed + +- **BREAKING:** The validator returned by `caip25EndowmentBuilder` now only verifies that there is single CAIP-25 caveat and nothing else([#5064](https://github.com/MetaMask/core/pull/5064)). + ## [1.1.2] ### Changed @@ -34,7 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#4962](https://github.com/MetaMask/core/pull/4962)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain@2.0.0...HEAD +[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.2...@metamask/multichain@2.0.0 [1.1.2]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.1...@metamask/multichain@1.1.2 [1.1.1]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.0...@metamask/multichain@1.1.1 [1.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.0.0...@metamask/multichain@1.1.0 diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 6ce6e734de..bd4ab5b064 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain", - "version": "1.1.2", + "version": "2.0.0", "description": "Provides types, helpers, adapters, and wrappers for facilitating CAIP Multichain sessions", "keywords": [ "MetaMask", From 6c2197f529a60878590e4de1e789500f7dec5adc Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:31:57 +0000 Subject: [PATCH 22/23] chore: bump nanoid dependency (#5073) ## Explanation Bump minor version of nanoid package in Permission Controller, Pemrission log controller and Approval controller to fix audit advisory: https://github.com/advisories/GHSA-mwcw-c2x4-8c55 ## References ## Changelog ### `@metamask/permission-controller` - ****: Bump `nanoid`pacakge to version ^3.3.8 ### `@metamask/approval-controller` - ****: Bump `nanoid`pacakge to version ^3.3.8 ### `@metamask/permission-log-controller` - ****: Bump `nanoid`pacakge to version ^3.3.8 ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/approval-controller/package.json | 2 +- packages/permission-controller/package.json | 2 +- packages/permission-log-controller/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/approval-controller/package.json b/packages/approval-controller/package.json index c736ff7edb..add06b5fae 100644 --- a/packages/approval-controller/package.json +++ b/packages/approval-controller/package.json @@ -50,7 +50,7 @@ "@metamask/base-controller": "^7.0.2", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", - "nanoid": "^3.1.31" + "nanoid": "^3.3.8" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index 3f9f2538e9..53180b44a6 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -55,7 +55,7 @@ "@types/deep-freeze-strict": "^1.1.0", "deep-freeze-strict": "^1.1.1", "immer": "^9.0.6", - "nanoid": "^3.1.31" + "nanoid": "^3.3.8" }, "devDependencies": { "@metamask/approval-controller": "^7.1.1", diff --git a/packages/permission-log-controller/package.json b/packages/permission-log-controller/package.json index 86248a5a33..31f1f1b27d 100644 --- a/packages/permission-log-controller/package.json +++ b/packages/permission-log-controller/package.json @@ -58,7 +58,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", - "nanoid": "^3.1.31", + "nanoid": "^3.3.8", "ts-jest": "^27.1.4", "typedoc": "^0.24.8", "typedoc-plugin-missing-exports": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 60e7d188b7..b0ce589157 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2133,7 +2133,7 @@ __metadata: "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" - nanoid: "npm:^3.1.31" + nanoid: "npm:^3.3.8" sinon: "npm:^9.2.4" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" @@ -3280,7 +3280,7 @@ __metadata: deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6" jest: "npm:^27.5.1" - nanoid: "npm:^3.1.31" + nanoid: "npm:^3.3.8" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" @@ -3303,7 +3303,7 @@ __metadata: deep-freeze-strict: "npm:^1.1.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" - nanoid: "npm:^3.1.31" + nanoid: "npm:^3.3.8" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" @@ -10092,12 +10092,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.1.31, nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" +"nanoid@npm:^3.1.31, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": + version: 3.3.8 + resolution: "nanoid@npm:3.3.8" bin: nanoid: bin/nanoid.cjs - checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 + checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 languageName: node linkType: hard From 99cd1a2ef53a32c6d6298338bf55213c66817a8c Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:13:31 +0100 Subject: [PATCH 23/23] fix: lock `KeyringController` mutex on `verifySeedPhrase` (#5077) ## Explanation The `KeyringController.verifySeedPhrase` method was not included in the mutable methods that lock the controller mutex because it doesn't change the state. Though, if another operation gets somehow overlapped (e.g. a consumer calls `addNewAccount`), the call to `verifySeedPhrase` can potentially fail. To fix this, this PR is moving verifySeedPhrase behind KeyringController's mutex. Since `addNewAccount` internally calls `verifySeedPhrase`, and having a lock on both would create a deadlock, the `verifySeedPhrase` implementation has been moved to an internal method. ## References ## Changelog ### `@metamask/keyring-controller` - **FIXED**: `verifySeedPhrase` is now mutually exclusive ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../src/KeyringController.ts | 95 +++++++++++-------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 06ed76bca3..5bef998de1 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -686,7 +686,7 @@ export class KeyringController extends BaseController< } const [addedAccountAddress] = await primaryKeyring.addAccounts(1); - await this.verifySeedPhrase(); + await this.#verifySeedPhrase(); return addedAccountAddress; }); @@ -1356,47 +1356,7 @@ export class KeyringController extends BaseController< * @returns Promise resolving to the seed phrase as Uint8Array. */ async verifySeedPhrase(): Promise { - const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as - | EthKeyring - | undefined; - if (!primaryKeyring) { - throw new Error('No HD keyring found.'); - } - - assertHasUint8ArrayMnemonic(primaryKeyring); - - const seedWords = primaryKeyring.mnemonic; - const accounts = await primaryKeyring.getAccounts(); - /* istanbul ignore if */ - if (accounts.length === 0) { - throw new Error('Cannot verify an empty keyring.'); - } - - // The HD Keyring Builder is a default keyring builder - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!; - - const hdKeyring = hdKeyringBuilder(); - // @ts-expect-error @metamask/eth-hd-keyring correctly handles - // Uint8Array seed phrases in the `deserialize` method. - await hdKeyring.deserialize({ - mnemonic: seedWords, - numberOfAccounts: accounts.length, - }); - const testAccounts = await hdKeyring.getAccounts(); - /* istanbul ignore if */ - if (testAccounts.length !== accounts.length) { - throw new Error('Seed phrase imported incorrect number of accounts.'); - } - - testAccounts.forEach((account: string, i: number) => { - /* istanbul ignore if */ - if (account.toLowerCase() !== accounts[i].toLowerCase()) { - throw new Error('Seed phrase imported different accounts.'); - } - }); - - return seedWords; + return this.#withControllerLock(async () => this.#verifySeedPhrase()); } /** @@ -1883,6 +1843,57 @@ export class KeyringController extends BaseController< this.#setUnlocked(); } + /** + * Internal non-exclusive method to verify the seed phrase. + * + * @returns A promise resolving to the seed phrase as Uint8Array. + */ + async #verifySeedPhrase(): Promise { + this.#assertControllerMutexIsLocked(); + + const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as + | EthKeyring + | undefined; + if (!primaryKeyring) { + throw new Error('No HD keyring found.'); + } + + assertHasUint8ArrayMnemonic(primaryKeyring); + + const seedWords = primaryKeyring.mnemonic; + const accounts = await primaryKeyring.getAccounts(); + /* istanbul ignore if */ + if (accounts.length === 0) { + throw new Error('Cannot verify an empty keyring.'); + } + + // The HD Keyring Builder is a default keyring builder + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!; + + const hdKeyring = hdKeyringBuilder(); + // @ts-expect-error @metamask/eth-hd-keyring correctly handles + // Uint8Array seed phrases in the `deserialize` method. + await hdKeyring.deserialize({ + mnemonic: seedWords, + numberOfAccounts: accounts.length, + }); + const testAccounts = await hdKeyring.getAccounts(); + /* istanbul ignore if */ + if (testAccounts.length !== accounts.length) { + throw new Error('Seed phrase imported incorrect number of accounts.'); + } + + testAccounts.forEach((account: string, i: number) => { + /* istanbul ignore if */ + if (account.toLowerCase() !== accounts[i].toLowerCase()) { + throw new Error('Seed phrase imported different accounts.'); + } + }); + + return seedWords; + } + /** * Get the updated array of each keyring's type and * accounts list.