From 604f3d873b7012ae419b05a8e777a174d63ea1e0 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT <16631641+tanguyenvn@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:17:16 +0700 Subject: [PATCH 1/4] Release/938.0.0 (#8601) ## Explanation Release passkey-controller. ## References ## 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 communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Release metadata-only changes (version bumps and changelog updates) with no runtime code modifications. > > **Overview** > Updates the monorepo release version to `938.0.0`. > > Publishes `@metamask/passkey-controller` as `1.0.0` by updating its package version and finalizing its changelog with a `1.0.0` release section and updated compare/release links. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b0c3347dc97c8eb373d94b2a9c7d93efc98ab1b3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 2 +- packages/passkey-controller/CHANGELOG.md | 5 ++++- packages/passkey-controller/package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8e413930e5f..32734f5aeee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "937.0.0", + "version": "938.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/passkey-controller/CHANGELOG.md b/packages/passkey-controller/CHANGELOG.md index 349b1314029..57ba935f7a2 100644 --- a/packages/passkey-controller/CHANGELOG.md +++ b/packages/passkey-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] + ### Added - Initial `@metamask/passkey-controller` ([#8422](https://github.com/MetaMask/core/pull/8422)): `PasskeyController` for WebAuthn passkey vault key protection (HKDF-derived keys, AES-256-GCM wrap/unwrap), PRF or `userHandle` derivation, challenge-keyed `CeremonyManager`, enrollment/unlock/renewal flows, `verifyPasskeyAuthentication`, selectors, and exported ceremony timing constants. @@ -21,4 +23,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Registration verification requires the credential `id`/`rawId` to match the credential id in authenticator data; vault wrapping key derivation uses that verified credential id so enrollment keys align with the stored credential. - Registration options request attestation conveyance `'none'` so clients are not asked for direct attestation formats the verifier does not implement (`none` and self-attested `packed` only). -[Unreleased]: https://github.com/MetaMask/core/ +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@1.0.0...HEAD +[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/passkey-controller@1.0.0 diff --git a/packages/passkey-controller/package.json b/packages/passkey-controller/package.json index 931a45cd83a..645e52b59bb 100644 --- a/packages/passkey-controller/package.json +++ b/packages/passkey-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/passkey-controller", - "version": "0.0.0", + "version": "1.0.0", "description": "Controller and utilities for passkey-based wallet unlock", "keywords": [ "Ethereum", From 12ba53af95221d3af5d394e7c88fa0bbc2a5c5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos?= Date: Tue, 28 Apr 2026 09:18:04 +0200 Subject: [PATCH 2/4] feat: adds getByPositionId (#8602) ## Explanation Expose GET /v1/traders/position/:positionId through SocialService as fetchPositionById, returning a single Position by ID. Reuses the existing Position type and PositionStruct validator; no new response types needed. ## References - Related to https://consensyssoftware.atlassian.net/jira/software/c/projects/TSA/boards/3368?assignee=5b58c0f5eda3e92ca73222ee&selectedIssue=TSA-461 ## 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 communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Additive change that introduces a new read-only API wrapper method with schema validation and test coverage; minimal impact on existing flows. > > **Overview** > Adds `SocialService.fetchPositionById` to retrieve a single `Position` via `GET /v1/traders/position/:positionId`, including URL-encoding, caching via `fetchQuery`, and response validation with the existing `PositionStruct`. > > Exposes the method through messenger action types/exports, introduces `FetchPositionByIdOptions` plus new error messages, and adds unit tests + changelog entry for the new endpoint wrapper. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 495c91e2c0c764291eefee98e09b78d76465e960. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/social-controllers/CHANGELOG.md | 4 + .../src/SocialService-method-action-types.ts | 15 ++++ .../src/SocialService.test.ts | 79 +++++++++++++++++++ .../social-controllers/src/SocialService.ts | 40 ++++++++++ packages/social-controllers/src/index.ts | 2 + .../src/social-constants.ts | 3 + .../social-controllers/src/social-types.ts | 5 ++ 7 files changed, 148 insertions(+) diff --git a/packages/social-controllers/CHANGELOG.md b/packages/social-controllers/CHANGELOG.md index 29651b6b974..09496f584df 100644 --- a/packages/social-controllers/CHANGELOG.md +++ b/packages/social-controllers/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `SocialService.fetchPositionById` method exposing `GET /v1/traders/position/:positionId`, returning a single `Position` by ID ([#8602](https://github.com/MetaMask/core/pull/8602)) + ## [2.1.0] ### Added diff --git a/packages/social-controllers/src/SocialService-method-action-types.ts b/packages/social-controllers/src/SocialService-method-action-types.ts index 1c9b729e31f..6d3daa7605e 100644 --- a/packages/social-controllers/src/SocialService-method-action-types.ts +++ b/packages/social-controllers/src/SocialService-method-action-types.ts @@ -82,6 +82,20 @@ export type SocialServiceFetchFollowersAction = { handler: SocialService['fetchFollowers']; }; +/** + * Fetches a single position by its unique ID. + * + * Calls `GET ${baseUrl}/traders/position/${positionId}`. + * + * @param options - Options bag. + * @param options.positionId - Unique position ID (UUID). + * @returns The position. + */ +export type SocialServiceFetchPositionByIdAction = { + type: `SocialService:fetchPositionById`; + handler: SocialService['fetchPositionById']; +}; + /** * Fetches the list of traders the current user is following. * @@ -136,6 +150,7 @@ export type SocialServiceMethodActions = | SocialServiceFetchOpenPositionsAction | SocialServiceFetchClosedPositionsAction | SocialServiceFetchFollowersAction + | SocialServiceFetchPositionByIdAction | SocialServiceFetchFollowingAction | SocialServiceFollowAction | SocialServiceUnfollowAction; diff --git a/packages/social-controllers/src/SocialService.test.ts b/packages/social-controllers/src/SocialService.test.ts index a9c159ce811..43defcc5fcd 100644 --- a/packages/social-controllers/src/SocialService.test.ts +++ b/packages/social-controllers/src/SocialService.test.ts @@ -628,6 +628,85 @@ describe('SocialService', () => { }); }); + describe('fetchPositionById', () => { + it('fetches position from correct endpoint', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + const result = await service.fetchPositionById({ + positionId: 'position-1', + }); + + expect(result).toStrictEqual(mockPosition); + expect(mockFetch).toHaveBeenCalledWith( + `${V1_URL}/traders/position/position-1`, + { headers: { Authorization: `Bearer ${MOCK_TOKEN}` } }, + ); + }); + + it('encodes the positionId in the URL', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + await service.fetchPositionById({ positionId: 'pos/with/slashes' }); + + expect(mockFetch).toHaveBeenCalledWith( + `${V1_URL}/traders/position/pos%2Fwith%2Fslashes`, + { headers: { Authorization: `Bearer ${MOCK_TOKEN}` } }, + ); + }); + + it('throws HttpError on non-ok response', async () => { + mockFetch.mockResolvedValue({ ok: false, status: 404 }); + + const service = createService(); + + await expect( + service.fetchPositionById({ positionId: 'position-1' }), + ).rejects.toThrow( + `${SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED}: 404`, + ); + }); + + it('throws when response schema is invalid', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ positionId: 123 }), + }); + + const service = createService(); + + await expect( + service.fetchPositionById({ positionId: 'position-1' }), + ).rejects.toThrow( + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE, + ); + }); + + it('returns cached result on repeated calls with same positionId', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockPosition), + }); + + const service = createService(); + await service.fetchPositionById({ positionId: 'position-1' }); + await service.fetchPositionById({ positionId: 'position-1' }); + + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); + describe('fetchFollowing', () => { const mockFollowingResponse = { following: [mockProfileSummary], diff --git a/packages/social-controllers/src/SocialService.ts b/packages/social-controllers/src/SocialService.ts index 02efdfd5a4e..2442c37ecf2 100644 --- a/packages/social-controllers/src/SocialService.ts +++ b/packages/social-controllers/src/SocialService.ts @@ -24,6 +24,7 @@ import { serviceName, SocialServiceErrorMessage } from './social-constants'; import type { FetchFollowersOptions, FetchLeaderboardOptions, + FetchPositionByIdOptions, FetchPositionsOptions, FetchTraderProfileOptions, FollowersResponse, @@ -31,6 +32,7 @@ import type { FollowOptions, FollowResponse, LeaderboardResponse, + Position, PositionsResponse, TraderProfileResponse, UnfollowOptions, @@ -174,6 +176,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'fetchClosedPositions', 'fetchFollowers', 'fetchFollowing', + 'fetchPositionById', 'follow', 'unfollow', ] as const; @@ -422,6 +425,43 @@ export class SocialService extends BaseDataService< return followersResponse; } + /** + * Fetches a single position by its unique ID. + * + * Calls `GET ${baseUrl}/traders/position/${positionId}`. + * + * @param options - Options bag. + * @param options.positionId - Unique position ID (UUID). + * @returns The position. + */ + async fetchPositionById( + options: FetchPositionByIdOptions, + ): Promise { + const { positionId } = options; + + const positionResponse = await this.fetchQuery({ + queryKey: [`${this.name}:fetchPositionById`, positionId], + queryFn: async () => { + const url = `${this.#v1Url}/traders/position/${encodeURIComponent(positionId)}`; + const authHeaders = await this.#getAuthHeaders(); + const response = await fetch(url, { headers: authHeaders }); + SocialService.#throwIfNotOk( + response, + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED, + ); + const positionData = await response.json(); + if (!is(positionData, PositionStruct)) { + throw new Error( + SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE, + ); + } + return positionData as Position; + }, + }); + + return positionResponse; + } + /** * Fetches the list of traders the current user is following. * diff --git a/packages/social-controllers/src/index.ts b/packages/social-controllers/src/index.ts index af4661c3401..5d08d155669 100644 --- a/packages/social-controllers/src/index.ts +++ b/packages/social-controllers/src/index.ts @@ -31,6 +31,7 @@ export type { SocialServiceFetchFollowingAction, SocialServiceFetchLeaderboardAction, SocialServiceFetchOpenPositionsAction, + SocialServiceFetchPositionByIdAction, SocialServiceFetchTraderProfileAction, SocialServiceFollowAction, SocialServiceUnfollowAction, @@ -40,6 +41,7 @@ export { TradeStruct } from './social-types'; export type { FetchFollowersOptions, FetchLeaderboardOptions, + FetchPositionByIdOptions, FetchPositionsOptions, FetchTraderProfileOptions, FollowersResponse, diff --git a/packages/social-controllers/src/social-constants.ts b/packages/social-controllers/src/social-constants.ts index e3acf0003c3..8da17127e90 100644 --- a/packages/social-controllers/src/social-constants.ts +++ b/packages/social-controllers/src/social-constants.ts @@ -27,4 +27,7 @@ export const SocialServiceErrorMessage = { UNFOLLOW_FAILED: 'SocialService: Unfollow request failed', UNFOLLOW_INVALID_RESPONSE: 'SocialService: Unfollow returned invalid response', + FETCH_POSITION_BY_ID_FAILED: 'SocialService: Position request failed', + FETCH_POSITION_BY_ID_INVALID_RESPONSE: + 'SocialService: Position returned invalid response', } as const; diff --git a/packages/social-controllers/src/social-types.ts b/packages/social-controllers/src/social-types.ts index efa5fd12502..c7de2384fb9 100644 --- a/packages/social-controllers/src/social-types.ts +++ b/packages/social-controllers/src/social-types.ts @@ -242,6 +242,11 @@ export type FetchFollowersOptions = { addressOrId: string; }; +export type FetchPositionByIdOptions = { + /** Unique position ID (UUID). */ + positionId: string; +}; + export type FollowOptions = { /** Array of wallet addresses or profile IDs to follow. */ targets: string[]; From c9472378a75da3f905ff03d140622baee8b74765 Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:59:41 +1200 Subject: [PATCH 3/4] Allow Advanced Permissions metadata in signtypeddata payload (#8603) ## Explanation https://github.com/MetaMask/core/pull/8526 adds tighter validation to signtypeddata v4 payloads, to ensure that no extraneous properties are added. This additional validation disallows Advanced Permissions `metadata` which is used to communicate the origin and justification of the permission. This change loosens the validation just enough to allow `metadata: { justification: string; origin: string }` as a property on the payload that is not used within the message encoding. ## References https://github.com/MetaMask/core/pull/8526 https://github.com/MetaMask/metamask-extension/issues/42181 ## 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 communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Touches security-adjacent request validation for typed-data signing; while the new `metadata` allowance is tightly constrained, any loosening here could affect input filtering behavior. > > **Overview** > Relaxes `signTypedData` (V4) payload validation to permit an additional top-level `metadata` field used by Advanced Permissions. > > `validateTypedMessageKeys` now explicitly allows `metadata` and enforces it is exactly `{ justification: string, origin: string }` (rejecting non-objects, missing/typed fields, or extra keys), with new unit tests covering the allowed and rejected cases; changelog updated accordingly. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 17f9432de8502ce6b04b01ab63d0828759f62367. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/eth-json-rpc-middleware/CHANGELOG.md | 4 + .../src/utils/validation.test.ts | 117 ++++++++++++++++++ .../src/utils/validation.ts | 28 ++++- 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index ed1da74e8cd..5f0f7ddd07d 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Allow Advanced Permissions `metadata` in signTypedData V4 requests ([#8603](https://github.com/MetaMask/core/pull/8603)) + ## [23.1.2] ### Changed diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts index 0b572b9a31d..230b476a4cf 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.test.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.test.ts @@ -160,5 +160,122 @@ describe('Validation Utils', () => { expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); }); + + describe('metadata', () => { + const baseTypedData = { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + }, + primaryType: 'EIP712Domain', + domain: {}, + message: {}, + }; + + it('does not throw when metadata has exactly justification and origin as strings', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).not.toThrow(); + }); + + it('does not throw when metadata is the only top-level key', () => { + const data = JSON.stringify({ + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).not.toThrow(); + }); + + it.each([ + ['null', null], + ['a string', 'not-an-object'], + ['a number', 42], + ['a boolean', true], + ['an array', ['justification', 'origin']], + ])('throws when metadata is %s', (_label, value) => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: value, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is missing justification', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is missing origin', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata.justification is not a string', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 123, + origin: 'https://example.com', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata.origin is not a string', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 123, + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata has an extraneous third key', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: { + justification: 'Permission to spend tokens', + origin: 'https://example.com', + extra: 'unexpected', + }, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + + it('throws when metadata is an empty object', () => { + const data = JSON.stringify({ + ...baseTypedData, + metadata: {}, + }); + + expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.'); + }); + }); }); }); diff --git a/packages/eth-json-rpc-middleware/src/utils/validation.ts b/packages/eth-json-rpc-middleware/src/utils/validation.ts index f65a6bc41ba..de3782fa67b 100644 --- a/packages/eth-json-rpc-middleware/src/utils/validation.ts +++ b/packages/eth-json-rpc-middleware/src/utils/validation.ts @@ -199,7 +199,10 @@ export function validateTypedDataForPrototypePollution(data: string): void { */ export function validateTypedMessageKeys(data: string): void { const parsedData = parseTypedMessage(data); - const allowedKeys = new Set(Object.keys(TYPED_MESSAGE_SCHEMA.properties)); + const allowedKeys = new Set([ + ...Object.keys(TYPED_MESSAGE_SCHEMA.properties), + 'metadata', + ]); const hasExtraneousKey = Object.keys(parsedData).some( (key) => !allowedKeys.has(key), ); @@ -207,4 +210,27 @@ export function validateTypedMessageKeys(data: string): void { if (hasExtraneousKey) { throw rpcErrors.invalidInput(); } + + // Advanced Permissions adds `metadata: { justification: string, origin: string }` to eth_signTypedData requests. + // see GatorPermissionsController.decodePermissionFromPermissionContextForOrigin for more details. + const { metadata } = parsedData as { metadata?: unknown }; + if (metadata !== undefined) { + if (typeof metadata !== 'object' || metadata === null) { + throw rpcErrors.invalidInput(); + } + + const { justification, origin } = metadata as { + justification?: unknown; + origin?: unknown; + }; + + if (typeof justification !== 'string' || typeof origin !== 'string') { + throw rpcErrors.invalidInput(); + } + + // we only need to check the keys length, because we already checked the known keys (justification and origin). + if (Object.keys(metadata).length !== 2) { + throw rpcErrors.invalidInput(); + } + } } From fac6fa105223c4c43bd45674edab9efbe6e4c761 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 28 Apr 2026 09:03:15 +0100 Subject: [PATCH 4/4] prevent crash from missing account (#8604) ## Explanation Stop crash that occurs when there is a missing entry in the `internalAccount` object. https://metamask.sentry.io/issues/7394639158/?project=273505&query=is%3Aunresolved%20Cannot%20read%20properties%20of%20undefined&referrer=issue-stream https://metamask.sentry.io/issues/6977774458/?project=273505&query=is%3Aunresolved%20Cannot%20read%20properties%20of%20undefined&referrer=issue-stream ## References ## 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 communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [X] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Small defensive change in a selector plus a unit test; behavior only changes for inconsistent state where an account ID is missing. > > **Overview** > Prevents `selectAssetsBySelectedAccountGroup` from crashing when an `accountTree` group references an account ID that is missing from `internalAccounts` by skipping those entries during account mapping. > > Adds a regression test covering the missing-account scenario and documents the fix in the assets-controllers changelog. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 78aa9aa0ff4107b685c534bf9af01967c791c703. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/assets-controllers/CHANGELOG.md | 4 ++++ .../src/selectors/token-selectors.test.ts | 12 ++++++++++++ .../src/selectors/token-selectors.ts | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index ccb00b0d13b..d48dd98eb5e 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Malicious tokens that slip through are caught by the periodic rescan (runs daily by default) - Bump `@metamask/transaction-controller` from `^64.3.0` to `^64.4.0` ([#8585](https://github.com/MetaMask/core/pull/8585)) +### Fixed + +- Fix `selectAssetsBySelectedAccountGroup` crashing when an account referenced in the account tree is missing from internal accounts ([#8604](https://github.com/MetaMask/core/pull/8604)) + ## [104.3.0] ### Added diff --git a/packages/assets-controllers/src/selectors/token-selectors.test.ts b/packages/assets-controllers/src/selectors/token-selectors.test.ts index 76c79fc55a6..ccbac2f2d21 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.test.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.test.ts @@ -877,6 +877,18 @@ describe('token-selectors', () => { expect(result).toStrictEqual(expectedMockResult); }); + it('skips accounts referenced in accountTree but missing from internalAccounts', () => { + const state = cloneDeep(mockedMergedState); + + state.accountTree.wallets['entropy:01K1TJY9QPSCKNBSVGZNG510GJ'].groups[ + 'entropy:01K1TJY9QPSCKNBSVGZNG510GJ/0' + ].accounts.push('non-existent-account-id'); + + const result = selectAssetsBySelectedAccountGroup(state); + + expect(result).toStrictEqual(expectedMockResult); + }); + it('returns no tokens if there is no selected account group', () => { const result = selectAssetsBySelectedAccountGroup({ ...mockedMergedState, diff --git a/packages/assets-controllers/src/selectors/token-selectors.ts b/packages/assets-controllers/src/selectors/token-selectors.ts index af409382d40..4b6e7dc2b7d 100644 --- a/packages/assets-controllers/src/selectors/token-selectors.ts +++ b/packages/assets-controllers/src/selectors/token-selectors.ts @@ -140,6 +140,10 @@ const selectAccountsToGroupIdMap = createAssetListSelector( for (const accountId of accounts) { const internalAccount = internalAccounts.accounts[accountId]; + if (!internalAccount) { + continue; + } + accountsMap[ // TODO: We would not need internalAccounts if evmTokens state had the accountId internalAccount.type.startsWith('eip155')