diff --git a/packages/chain-agnostic-permission/CHANGELOG.md b/packages/chain-agnostic-permission/CHANGELOG.md index cc427f145d4..55c3685517f 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 to enable custom account ordering within session scopes ([#8255](https://github.com/MetaMask/core/pull/8255)) + ### 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..c23d7633513 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,118 @@ 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, }), ); }; diff --git a/packages/multichain-api-middleware/CHANGELOG.md b/packages/multichain-api-middleware/CHANGELOG.md index 1ff8143605a..1983d0d42bb 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 enable custom account ordering in session scopes ([#8255](https://github.com/MetaMask/core/pull/8255)) + ### 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..54709e7089e 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,8 @@ 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 +124,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..d8a24a967d3 100644 --- a/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts +++ b/packages/multichain-api-middleware/src/handlers/wallet-getSession.ts @@ -8,6 +8,7 @@ import { getSessionScopes, } from '@metamask/chain-agnostic-permission'; import type { Caveat } from '@metamask/permission-controller'; +import type { CaipAccountId } from '@metamask/utils'; import type { CaipChainId, JsonRpcRequest, @@ -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, }, };