From 5a9895527454e19a9e9ba551ec800bc1405c8f38 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Mar 2026 13:33:24 -0700 Subject: [PATCH 1/3] add sortAccountIdsByLastSelected to getSessionScopes --- .../chain-agnostic-permission/CHANGELOG.md | 4 + ...permission-operator-session-scopes.test.ts | 105 ++++++++++++++++++ ...caip-permission-operator-session-scopes.ts | 18 ++- 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/chain-agnostic-permission/CHANGELOG.md b/packages/chain-agnostic-permission/CHANGELOG.md index cc427f145d4..a13d0ea9cb9 100644 --- a/packages/chain-agnostic-permission/CHANGELOG.md +++ b/packages/chain-agnostic-permission/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `sortAccountIdsByLastSelected` parameter to `getSessionScopes` function that is applied to each scope's `accounts` value when provided + ### Changed - Bump `@metamask/permission-controller` from `^12.2.0` to `^12.2.1` ([#8225](https://github.com/MetaMask/core/pull/8225)) diff --git a/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.test.ts b/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.test.ts index 105ee20526d..a5547e3a8d6 100644 --- a/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.test.ts +++ b/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.test.ts @@ -39,6 +39,7 @@ describe('CAIP-25 session scopes adapters', () => { describe('getSessionScopes', () => { const getNonEvmSupportedMethods = jest.fn(); + const mockSortAccountIdsByLastSelected = jest.fn(); it('returns a NormalizedScopesObject for the wallet scope', () => { const result = getSessionScopes( @@ -203,6 +204,110 @@ describe('CAIP-25 session scopes adapters', () => { }, }); }); + + it('sorts accounts using sortAccountIdsByLastSelected when provided', () => { + const unsortedAccounts = ['eip155:1:0xbeef', 'eip155:1:0xdead']; + const sortedAccounts = ['eip155:1:0xdead', 'eip155:1:0xbeef']; + + mockSortAccountIdsByLastSelected.mockReturnValue(sortedAccounts); + + const result = getSessionScopes( + { + requiredScopes: { + 'eip155:1': { + accounts: unsortedAccounts, + }, + }, + optionalScopes: {}, + }, + { + getNonEvmSupportedMethods, + sortAccountIdsByLastSelected: mockSortAccountIdsByLastSelected, + }, + ); + + expect(mockSortAccountIdsByLastSelected).toHaveBeenCalledWith(unsortedAccounts); + expect(result).toStrictEqual({ + 'eip155:1': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: sortedAccounts, + }, + }); + }); + + it('does not sort accounts when sortAccountIdsByLastSelected is not provided', () => { + const accounts = ['eip155:1:0xbeef', 'eip155:1:0xdead']; + + const result = getSessionScopes( + { + requiredScopes: { + 'eip155:1': { + accounts, + }, + }, + optionalScopes: {}, + }, + { + getNonEvmSupportedMethods, + }, + ); + + expect(mockSortAccountIdsByLastSelected).not.toHaveBeenCalled(); + expect(result).toStrictEqual({ + 'eip155:1': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts, // Original order preserved + }, + }); + }); + + it('sorts accounts in both required and optional scopes', () => { + const unsortedAccounts1 = ['eip155:1:0xbeef', 'eip155:1:0xdead']; + const unsortedAccounts2 = ['eip155:137:0xcafe', 'eip155:137:0xbabe']; + const sortedAccounts1 = ['eip155:1:0xdead', 'eip155:1:0xbeef']; + const sortedAccounts2 = ['eip155:137:0xbabe', 'eip155:137:0xcafe']; + + mockSortAccountIdsByLastSelected + .mockReturnValueOnce(sortedAccounts1) + .mockReturnValueOnce(sortedAccounts2); + + const result = getSessionScopes( + { + requiredScopes: { + 'eip155:1': { + accounts: unsortedAccounts1, + }, + }, + optionalScopes: { + 'eip155:137': { + accounts: unsortedAccounts2, + }, + }, + }, + { + getNonEvmSupportedMethods, + sortAccountIdsByLastSelected: mockSortAccountIdsByLastSelected, + }, + ); + + expect(mockSortAccountIdsByLastSelected).toHaveBeenCalledTimes(2); + expect(mockSortAccountIdsByLastSelected).toHaveBeenNthCalledWith(1, unsortedAccounts1); + expect(mockSortAccountIdsByLastSelected).toHaveBeenNthCalledWith(2, unsortedAccounts2); + expect(result).toStrictEqual({ + 'eip155:1': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: sortedAccounts1, + }, + 'eip155:137': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: sortedAccounts2, + }, + }); + }); }); describe('getPermittedAccountsForScopes', () => { diff --git a/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.ts b/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.ts index 16fb6983cf1..6b45f4e42fd 100644 --- a/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.ts +++ b/packages/chain-agnostic-permission/src/operators/caip-permission-operator-session-scopes.ts @@ -45,14 +45,19 @@ export const getInternalScopesObject = ( * @param internalScopesObject - The InternalScopesObject to convert. * @param hooks - An object containing the following properties: * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. + * @param [hooks.sortAccountIdsByLastSelected] - Optional function that accepts an array of CaipAccountId and returns an array of CaipAccountId sorted by last selected. * @returns A NormalizedScopesObject. */ const getNormalizedScopesObject = ( internalScopesObject: InternalScopesObject, { getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }: { getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; + sortAccountIdsByLastSelected?: ( + accounts: CaipAccountId[], + ) => CaipAccountId[]; }, ) => { const normalizedScopes: NormalizedScopesObject = {}; @@ -83,10 +88,14 @@ const getNormalizedScopesObject = ( notifications = []; } + const sortedAccounts = sortAccountIdsByLastSelected + ? sortAccountIdsByLastSelected(accounts) + : accounts; + normalizedScopes[scopeString] = { methods, notifications, - accounts, + accounts: sortedAccounts, }; }, ); @@ -101,6 +110,7 @@ const getNormalizedScopesObject = ( * @param caip25CaveatValue - The CAIP-25 CaveatValue to convert. * @param hooks - An object containing the following properties: * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. + * @param [hooks.sortAccountIdsByLastSelected] - Optional function that accepts an array of CaipAccountId and returns an array of CaipAccountId sorted by last selected. * @returns A NormalizedScopesObject. */ export const getSessionScopes = ( @@ -110,16 +120,22 @@ export const getSessionScopes = ( >, { getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }: { getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; + sortAccountIdsByLastSelected?: ( + accounts: CaipAccountId[], + ) => CaipAccountId[]; }, ) => { return mergeNormalizedScopes( getNormalizedScopesObject(caip25CaveatValue.requiredScopes, { getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }), getNormalizedScopesObject(caip25CaveatValue.optionalScopes, { getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }), ); }; From 1b9499f1a8a8b47552024e8ea741551bd055c813 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Mar 2026 13:35:06 -0700 Subject: [PATCH 2/3] update wallet_createSession and wallet_getSession --- packages/multichain-api-middleware/CHANGELOG.md | 4 ++++ .../src/handlers/wallet-createSession.test.ts | 3 +++ .../src/handlers/wallet-createSession.ts | 6 ++++++ .../src/handlers/wallet-getSession.test.ts | 6 +++++- .../src/handlers/wallet-getSession.ts | 7 +++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/multichain-api-middleware/CHANGELOG.md b/packages/multichain-api-middleware/CHANGELOG.md index 1ff8143605a..9859a24debe 100644 --- a/packages/multichain-api-middleware/CHANGELOG.md +++ b/packages/multichain-api-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add required `sortAccountIdsByLastSelected` hook to `wallet_getSession` and `wallet_createSession` handlers to ensure each scope's `accounts` value is returned with the most recently selected account IDs ordered first + ### Changed - Bump `@metamask/permission-controller` from `^12.2.0` to `^12.2.1` ([#8225](https://github.com/MetaMask/core/pull/8225)) diff --git a/packages/multichain-api-middleware/src/handlers/wallet-createSession.test.ts b/packages/multichain-api-middleware/src/handlers/wallet-createSession.test.ts index c187fe96681..8b2c2a5b4f9 100644 --- a/packages/multichain-api-middleware/src/handlers/wallet-createSession.test.ts +++ b/packages/multichain-api-middleware/src/handlers/wallet-createSession.test.ts @@ -104,6 +104,7 @@ const createMockedHandler = () => { sessionProperties?: Record; }>; const getNonEvmAccountAddresses = jest.fn().mockReturnValue([]); + const sortAccountIdsByLastSelected = jest.fn((accounts) => accounts); const handler = ( request: JsonRpcRequest & { origin: string }, ) => @@ -114,6 +115,7 @@ const createMockedHandler = () => { getNonEvmSupportedMethods, isNonEvmScopeSupported, getNonEvmAccountAddresses, + sortAccountIdsByLastSelected, trackSessionCreatedEvent, }); @@ -128,6 +130,7 @@ const createMockedHandler = () => { getNonEvmSupportedMethods, isNonEvmScopeSupported, getNonEvmAccountAddresses, + sortAccountIdsByLastSelected, handler, }; }; diff --git a/packages/multichain-api-middleware/src/handlers/wallet-createSession.ts b/packages/multichain-api-middleware/src/handlers/wallet-createSession.ts index cf71fd81390..5844d6cfdda 100644 --- a/packages/multichain-api-middleware/src/handlers/wallet-createSession.ts +++ b/packages/multichain-api-middleware/src/handlers/wallet-createSession.ts @@ -66,6 +66,7 @@ const SOLANA_CAIP_CHAIN_ID = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; * @param hooks.getNonEvmSupportedMethods - The hook that returns the supported methods for a non EVM scope. * @param hooks.isNonEvmScopeSupported - The hook that returns true if a non EVM scope is supported. * @param hooks.getNonEvmAccountAddresses - The hook that returns a list of CaipAccountIds that are supported for a CaipChainId. + * @param hooks.sortAccountIdsByLastSelected - A function that accepts an array of CaipAccountId and returns an array of CaipAccountId sorted by last selected. * @param hooks.trackSessionCreatedEvent - An optional hook for platform specific logic to run. Can be undefined. * @returns A promise with wallet_createSession handler */ @@ -87,6 +88,9 @@ async function walletCreateSessionHandler( getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; isNonEvmScopeSupported: (scope: CaipChainId) => boolean; getNonEvmAccountAddresses: (scope: CaipChainId) => CaipAccountId[]; + sortAccountIdsByLastSelected: ( + accounts: CaipAccountId[], + ) => CaipAccountId[]; trackSessionCreatedEvent?: ( approvedCaip25CaveatValue: Caip25CaveatValue, ) => void; @@ -263,6 +267,7 @@ async function walletCreateSessionHandler( const sessionScopes = getSessionScopes(approvedCaip25CaveatValue, { getNonEvmSupportedMethods: hooks.getNonEvmSupportedMethods, + sortAccountIdsByLastSelected: hooks.sortAccountIdsByLastSelected, }); const { sessionProperties: approvedSessionProperties = {} } = @@ -290,6 +295,7 @@ export const walletCreateSession = { getNonEvmSupportedMethods: true, isNonEvmScopeSupported: true, getNonEvmAccountAddresses: true, + sortAccountIdsByLastSelected: true, trackSessionCreatedEvent: true, }, }; diff --git a/packages/multichain-api-middleware/src/handlers/wallet-getSession.test.ts b/packages/multichain-api-middleware/src/handlers/wallet-getSession.test.ts index 01668df4dd2..56bb146016d 100644 --- a/packages/multichain-api-middleware/src/handlers/wallet-getSession.test.ts +++ b/packages/multichain-api-middleware/src/handlers/wallet-getSession.test.ts @@ -23,6 +23,7 @@ const createMockedHandler = () => { const next = jest.fn(); const end = jest.fn(); const getNonEvmSupportedMethods = jest.fn(); + const sortAccountIdsByLastSelected = jest.fn((accounts) => accounts); const getCaveatForOrigin = jest.fn().mockReturnValue({ value: { requiredScopes: { @@ -54,6 +55,7 @@ const createMockedHandler = () => { walletGetSession.implementation(request, response, next, end, { getCaveatForOrigin, getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }); return { @@ -62,6 +64,7 @@ const createMockedHandler = () => { end, getCaveatForOrigin, getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, handler, }; }; @@ -96,7 +99,7 @@ describe('wallet_getSession', () => { }); it('gets the session scopes from the CAIP-25 caveat value', async () => { - const { handler, getNonEvmSupportedMethods } = createMockedHandler(); + const { handler, getNonEvmSupportedMethods, sortAccountIdsByLastSelected } = createMockedHandler(); await handler(baseRequest); expect(chainAgnosticPermissionModule.getSessionScopes).toHaveBeenCalledWith( @@ -120,6 +123,7 @@ describe('wallet_getSession', () => { }, { getNonEvmSupportedMethods, + sortAccountIdsByLastSelected, }, ); }); diff --git a/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts b/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts index e9f7880071c..449d1bfc83a 100644 --- a/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts +++ b/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts @@ -2,6 +2,7 @@ import type { Caip25CaveatValue, NormalizedScopesObject, } from '@metamask/chain-agnostic-permission'; +import type { CaipAccountId } from '@metamask/utils'; import { Caip25CaveatType, Caip25EndowmentPermissionName, @@ -27,6 +28,7 @@ import type { * @param hooks - The hooks object. * @param hooks.getCaveatForOrigin - Function to retrieve a caveat for the origin. * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. + * @param hooks.sortAccountIdsByLastSelected - A function that accepts an array of CaipAccountId and returns an array of CaipAccountId sorted by last selected. * @returns Nothing. */ async function walletGetSessionHandler( @@ -40,6 +42,9 @@ async function walletGetSessionHandler( caveatType: string, ) => Caveat; getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; + sortAccountIdsByLastSelected: ( + accounts: CaipAccountId[], + ) => CaipAccountId[]; }, ) { let caveat; @@ -60,6 +65,7 @@ async function walletGetSessionHandler( response.result = { sessionScopes: getSessionScopes(caveat.value, { getNonEvmSupportedMethods: hooks.getNonEvmSupportedMethods, + sortAccountIdsByLastSelected: hooks.sortAccountIdsByLastSelected, }), }; return end(); @@ -71,5 +77,6 @@ export const walletGetSession = { hookNames: { getCaveatForOrigin: true, getNonEvmSupportedMethods: true, + sortAccountIdsByLastSelected: true, }, }; From f3055a72b1a70efdf79e75c2e299eb60c4a269ae Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Mar 2026 13:38:38 -0700 Subject: [PATCH 3/3] changelog --- packages/chain-agnostic-permission/CHANGELOG.md | 2 +- packages/multichain-api-middleware/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/chain-agnostic-permission/CHANGELOG.md b/packages/chain-agnostic-permission/CHANGELOG.md index a13d0ea9cb9..55c3685517f 100644 --- a/packages/chain-agnostic-permission/CHANGELOG.md +++ b/packages/chain-agnostic-permission/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add optional `sortAccountIdsByLastSelected` parameter to `getSessionScopes` function that is applied to each scope's `accounts` value when provided +- Add optional `sortAccountIdsByLastSelected` parameter to `getSessionScopes` function to enable custom account ordering within session scopes ([#8255](https://github.com/MetaMask/core/pull/8255)) ### Changed diff --git a/packages/multichain-api-middleware/CHANGELOG.md b/packages/multichain-api-middleware/CHANGELOG.md index 9859a24debe..1983d0d42bb 100644 --- a/packages/multichain-api-middleware/CHANGELOG.md +++ b/packages/multichain-api-middleware/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add required `sortAccountIdsByLastSelected` hook to `wallet_getSession` and `wallet_createSession` handlers to ensure each scope's `accounts` value is returned with the most recently selected account IDs ordered first +- Add required `sortAccountIdsByLastSelected` hook to `wallet_getSession` and `wallet_createSession` handlers to enable custom account ordering in session scopes ([#8255](https://github.com/MetaMask/core/pull/8255)) ### Changed