Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type CreateSessionParams, getDefaultTransport, type Transport, type TransportRequest, type TransportResponse } from '@metamask/multichain-api-client';
import type { CaipAccountId } from '@metamask/utils';
import type { ExtendedTransport, RPCAPI, Scope, SessionData } from 'src/domain';
import { addValidAccounts, getOptionalScopes, getValidAccounts } from 'src/multichain/utils';
import { addValidAccounts, getOptionalScopes, getValidAccounts, isSameScopesAndAccounts } from 'src/multichain/utils';

const DEFAULT_REQUEST_TIMEOUT = 60 * 1000;

Expand Down Expand Up @@ -34,8 +34,9 @@ export class DefaultTransport implements ExtendedTransport {
if (walletSession && options) {
const currentScopes = Object.keys(walletSession?.sessionScopes ?? {}) as Scope[];
const proposedScopes = options?.scopes ?? [];
const isSameScopes = currentScopes.every((scope) => proposedScopes.includes(scope)) && proposedScopes.every((scope) => currentScopes.includes(scope));
if (!isSameScopes) {
const proposedCaipAccountIds = options?.caipAccountIds ?? [];
const hasSameScopesAndAccounts = isSameScopesAndAccounts(currentScopes, proposedScopes, walletSession, proposedCaipAccountIds);
if (!hasSameScopesAndAccounts) {
await this.request({ method: 'wallet_revokeSession', params: walletSession }, this.#defaultRequestOptions);
const optionalScopes = addValidAccounts(getOptionalScopes(options?.scopes ?? []), getValidAccounts(options?.caipAccountIds ?? []));
const sessionRequest: CreateSessionParams<RPCAPI> = { optionalScopes };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';

import { createLogger, type ExtendedTransport, type RPCAPI, type Scope, type SessionData, type StoreAdapter } from '../../../domain';
import type { CaipAccountId } from '@metamask/utils';
import { addValidAccounts, getOptionalScopes, getValidAccounts } from '../../utils';
import { addValidAccounts, getOptionalScopes, getValidAccounts, isSameScopesAndAccounts } from '../../utils';

const DEFAULT_REQUEST_TIMEOUT = 60 * 1000;
const CONNECTION_GRACE_PERIOD = 60 * 1000;
Expand Down Expand Up @@ -111,8 +111,9 @@ export class MWPTransport implements ExtendedTransport {
if (walletSession && options) {
const currentScopes = Object.keys(walletSession?.sessionScopes ?? {}) as Scope[];
const proposedScopes = options?.scopes ?? [];
const isSameScopes = currentScopes.every((scope) => proposedScopes.includes(scope)) && proposedScopes.every((scope) => currentScopes.includes(scope));
if (!isSameScopes) {
const proposedCaipAccountIds = options?.caipAccountIds ?? [];
const hasSameScopesAndAccounts = isSameScopesAndAccounts(currentScopes, proposedScopes, walletSession, proposedCaipAccountIds);
if (!hasSameScopesAndAccounts) {
const optionalScopes = addValidAccounts(getOptionalScopes(options?.scopes ?? []), getValidAccounts(options?.caipAccountIds ?? []));
const sessionRequest: CreateSessionParams<RPCAPI> = { optionalScopes };
const response = await this.request({ method: 'wallet_createSession', params: sessionRequest });
Expand Down
218 changes: 218 additions & 0 deletions packages/sdk-multichain/src/multichain/utils/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Tests require it */
import * as t from 'vitest';
import { vi } from 'vitest';
import type { CaipAccountId } from '@metamask/utils';
import packageJson from '../../../package.json';
import type { MultichainOptions } from '../../domain/multichain';
import { getPlatformType, isMetamaskExtensionInstalled, PlatformType } from '../../domain/platform';
import type { Scope } from '../../domain';
import * as utils from '.';

vi.mock('../../domain/platform', async () => {
Expand Down Expand Up @@ -277,4 +279,220 @@ t.describe('Utils', () => {
t.expect(isMetamaskExtensionInstalled()).toBe(false);
});
});

t.describe('isSameScopesAndAccounts', () => {
const mockWalletSession = {
sessionScopes: {
'eip155:1': {
methods: ['eth_sendTransaction'],
notifications: ['chainChanged'],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
'eip155:137': {
methods: ['eth_sendTransaction'],
notifications: ['chainChanged'],
accounts: ['eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'],
},
},
} as any;

t.it('should return true when scopes and accounts match exactly', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890',
'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
);

t.expect(result).toBe(true);
});

t.it('should return false when scopes do not match', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:1', 'eip155:56']; // Different scope
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890',
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
);

t.expect(result).toBe(false);
});

t.it('should return false when proposed accounts are not included in existing session', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890',
'eip155:1:0x9999999999999999999999999999999999999999', // Not in session
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
);

t.expect(result).toBe(false);
});

t.it('should return true when proposed accounts are subset of existing session accounts', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890', // Only one account
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
);

t.expect(result).toBe(true);
});

t.it('should return true when no accounts are proposed and scopes match', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedCaipAccountIds: CaipAccountId[] = [];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
)

t.expect(result).toBe(true);
});

t.it('should handle empty session scopes', () => {
const emptySession = { sessionScopes: {} } as any;
const currentScopes: Scope[] = [];
const proposedScopes: Scope[] = [];
const proposedCaipAccountIds: CaipAccountId[] = [];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
emptySession,
proposedCaipAccountIds,
);

t.expect(result).toBe(true);
});

t.it('should handle scope objects without accounts property', () => {
const sessionWithoutAccounts = {
sessionScopes: {
'eip155:1': {
methods: ['eth_sendTransaction'],
notifications: ['chainChanged'],
// No accounts property
},
},
} as any;

const currentScopes: Scope[] = ['eip155:1'];
const proposedScopes: Scope[] = ['eip155:1'];
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
sessionWithoutAccounts,
proposedCaipAccountIds,
);

t.expect(result).toBe(false);
});

t.it('should handle scope objects with empty accounts array', () => {
const sessionWithEmptyAccounts = {
sessionScopes: {
'eip155:1': {
methods: ['eth_sendTransaction'],
notifications: ['chainChanged'],
accounts: [],
},
},
} as any;

const currentScopes: Scope[] = ['eip155:1'];
const proposedScopes: Scope[] = ['eip155:1'];
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
sessionWithEmptyAccounts,
proposedCaipAccountIds,
);

t.expect(result).toBe(false);
});

t.it('should return true when scopes have different order but same content', () => {
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
const proposedScopes: Scope[] = ['eip155:137', 'eip155:1']; // Different order
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890',
'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
mockWalletSession,
proposedCaipAccountIds,
);

t.expect(result).toBe(true);
});

t.it('should handle multiple accounts in same scope', () => {
const sessionWithMultipleAccounts = {
sessionScopes: {
'eip155:1': {
methods: ['eth_sendTransaction'],
notifications: ['chainChanged'],
accounts: [
'eip155:1:0x1234567890123456789012345678901234567890',
'eip155:1:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
],
},
},
} as any;

const currentScopes: Scope[] = ['eip155:1'];
const proposedScopes: Scope[] = ['eip155:1'];
const proposedCaipAccountIds = [
'eip155:1:0x1234567890123456789012345678901234567890',
] as CaipAccountId[];

const result = utils.isSameScopesAndAccounts(
currentScopes,
proposedScopes,
sessionWithMultipleAccounts,
proposedCaipAccountIds,
);

t.expect(result).toBe(true);
});
});
});
33 changes: 33 additions & 0 deletions packages/sdk-multichain/src/multichain/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ export function setupDappMetadata(options: MultichainOptions): MultichainOptions
return options;
}

/**
* Enhanced scope checking function that validates both scopes and accounts
* @param currentScopes - Current scopes from the existing session
* @param proposedScopes - Proposed scopes from the connect options
* @param walletSession - The existing wallet session data
* @param proposedCaipAccountIds - Proposed account IDs from the connect options
* @returns true if scopes and accounts match, false otherwise
*/
export function isSameScopesAndAccounts(
currentScopes: Scope[],
proposedScopes: Scope[],
walletSession: SessionData,
proposedCaipAccountIds: CaipAccountId[],
): boolean {
const isSameScopes =
currentScopes.every((scope) => proposedScopes.includes(scope)) &&
proposedScopes.every((scope) => currentScopes.includes(scope));

if (!isSameScopes) {
return false;
}

const existingAccountIds: CaipAccountId[] = Object.values(walletSession.sessionScopes)
.filter(({ accounts }) => Boolean(accounts))
.flatMap(({ accounts }) => accounts ?? []);

const allProposedAccountsIncluded = proposedCaipAccountIds.every(
(proposedAccountId) => existingAccountIds.includes(proposedAccountId),
);

return allProposedAccountsIncluded;
}

export function getValidAccounts(caipAccountIds: CaipAccountId[]) {
return caipAccountIds.reduce<ReturnType<typeof parseCaipAccountId>[]>((caipAccounts, caipAccountId) => {
try {
Expand Down
Loading