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')