Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(walletconnect): migrate to WalletKit and implement multi-chain support #13515

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
60f64e0
wip
abretonc7s Feb 14, 2025
b62cc6c
Merge remote-tracking branch 'origin/main' into wcmigration
abretonc7s Feb 14, 2025
91e9fca
wip
abretonc7s Feb 14, 2025
db38979
wip
abretonc7s Feb 14, 2025
c34519e
wip
abretonc7s Feb 14, 2025
4bdfdf3
wip
abretonc7s Feb 14, 2025
b9a627c
wip
abretonc7s Feb 14, 2025
4ce6c94
unit tests
abretonc7s Feb 14, 2025
873bc8b
test
abretonc7s Feb 14, 2025
eb82eb2
Merge remote-tracking branch 'origin/main' into wcmigration
abretonc7s Feb 14, 2025
8b4fb9f
wip
abretonc7s Feb 14, 2025
71a34da
cleanup
abretonc7s Feb 14, 2025
01c94ca
wip
abretonc7s Feb 14, 2025
1aaf1b2
cleanup
abretonc7s Feb 14, 2025
952a7cc
cleanup
abretonc7s Feb 14, 2025
86abe68
pr comments
chakra-guy Feb 18, 2025
72a5001
added tests
chakra-guy Feb 18, 2025
4c9e226
lint fix
chakra-guy Feb 18, 2025
a0c6439
lint fix
chakra-guy Feb 18, 2025
88a6ba2
pr comment
chakra-guy Feb 19, 2025
3ae30be
Revert "pr comment"
chakra-guy Feb 19, 2025
038d787
added back required deps
chakra-guy Feb 19, 2025
b3f0e23
Merge remote-tracking branch 'origin/main' into wcmigration
chakra-guy Feb 19, 2025
41e0566
pr comments
chakra-guy Feb 20, 2025
b0b869a
added more tests
chakra-guy Feb 20, 2025
12a6bd4
Merge branch 'main' into wcmigration
chakra-guy Feb 20, 2025
fab4c90
Merge branch 'main' into wcmigration
chakra-guy Feb 20, 2025
bde6638
fix ui
chakra-guy Feb 20, 2025
06aa5e4
Merge branch 'main' into wcmigration
chakra-guy Feb 20, 2025
2c81fb4
Merge branch 'wcmigration' of https://github.com/MetaMask/metamask-mo…
chakra-guy Feb 20, 2025
4ac085e
Revert "fix ui"
chakra-guy Feb 20, 2025
8a8bf94
fix connection mismatch bug
chakra-guy Feb 21, 2025
8d16f70
Merge branch 'main' into wcmigration
chakra-guy Feb 21, 2025
78dd6b2
Merge branch 'main' into wcmigration
chakra-guy Feb 21, 2025
2e82622
fix transaction confirm ui label
chakra-guy Feb 21, 2025
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
2 changes: 2 additions & 0 deletions app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const ApprovalTagUrl = ({
title = prefixUrlWithProtocol(origin);
} else if (url) {
title = prefixUrlWithProtocol(getHost(url));
} else if (origin) {
title = prefixUrlWithProtocol(getHost(origin));
} else {
title = '';
}
Expand Down
2 changes: 2 additions & 0 deletions app/core/Multichain/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import BTC from '../../images/bitcoin-logo.png';
import SOL from '../../images/solana-logo.png';

export const EVM_IDENTIFIER = 'eip155';

// Image imports for React Native rendering
export const MULTICHAIN_TOKEN_IMAGES = {
[BtcScope.Mainnet]: BTC,
Expand Down
165 changes: 165 additions & 0 deletions app/core/Permissions/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {
getPermittedAccounts,
getPermittedChains,
switchActiveAccounts,
} from './index';
import { errorCodes as rpcErrorCodes } from '@metamask/rpc-errors';
import { RestrictedMethods, CaveatTypes } from './constants';
import { PermissionKeys } from './specifications';
import ImportedEngine from '../Engine';

// Mock dependencies
jest.mock('../Engine', () => ({
context: {
PermissionController: {
executeRestrictedMethod: jest.fn(),
getCaveat: jest.fn(),
updateCaveat: jest.fn(),
},
AccountsController: {
getSelectedAccount: jest.fn(),
},
},
}));

// Type assertion for mocked Engine
const Engine = ImportedEngine as jest.Mocked<any>;

// Mock normalizeOrigin from wc-utils
jest.mock('../WalletConnect/wc-utils', () => ({
normalizeOrigin: jest.fn((hostname) => hostname),
}));

describe('Permission Management Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('getPermittedAccounts', () => {
it('returns accounts from PermissionController for external hostname', async () => {
const hostname = 'example.com';
Engine.context.PermissionController.executeRestrictedMethod.mockResolvedValue([
'0xabc',
'0xdef',
]);

const result = await getPermittedAccounts(hostname);

expect(Engine.context.PermissionController.executeRestrictedMethod).toHaveBeenCalledWith(
hostname,
RestrictedMethods.eth_accounts,
);
expect(result).toEqual(['0xabc', '0xdef']);
});

it('returns empty array on unauthorized error', async () => {
const hostname = 'example.com';
Engine.context.PermissionController.executeRestrictedMethod.mockRejectedValue({
code: rpcErrorCodes.provider.unauthorized,
});

const result = await getPermittedAccounts(hostname);

expect(result).toEqual([]);
});

it('throws on unexpected error', async () => {
const hostname = 'example.com';
const error = new Error('Unexpected error');
Engine.context.PermissionController.executeRestrictedMethod.mockRejectedValue(error);

await expect(getPermittedAccounts(hostname)).rejects.toThrow(error);
});
});

describe('getPermittedChains', () => {
it('returns formatted chains from caveat', async () => {
const hostname = 'example.com';
Engine.context.PermissionController.getCaveat.mockReturnValue({
value: ['1', '2', 'invalid'],
});

const result = await getPermittedChains(hostname);

expect(Engine.context.PermissionController.getCaveat).toHaveBeenCalledWith(
hostname,
PermissionKeys.permittedChains,
CaveatTypes.restrictNetworkSwitching,
);
expect(result).toEqual(['eip155:1', 'eip155:2']); // Should filter out 'invalid'
});

it('returns empty array if no caveat exists', async () => {
const hostname = 'example.com';
Engine.context.PermissionController.getCaveat.mockReturnValue(undefined);

const result = await getPermittedChains(hostname);

expect(result).toEqual([]);
});

it('returns empty array if caveat value is not an array', async () => {
const hostname = 'example.com';
Engine.context.PermissionController.getCaveat.mockReturnValue({
value: 'not-an-array',
});

const result = await getPermittedChains(hostname);

expect(result).toEqual([]);
});
});

describe('switchActiveAccounts', () => {
it('reorders accounts to make the specified account active', () => {
const hostname = 'example.com';
const accAddress = '0xdef';
Engine.context.PermissionController.getCaveat.mockReturnValue({
value: ['0xabc', '0xdef', '0x123'],
});

switchActiveAccounts(hostname, accAddress);

expect(Engine.context.PermissionController.getCaveat).toHaveBeenCalledWith(
hostname,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
);
expect(Engine.context.PermissionController.updateCaveat).toHaveBeenCalledWith(
hostname,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
['0xdef', '0xabc', '0x123'],
);
});

it('throws if account is not permitted', () => {
const hostname = 'example.com';
const accAddress = '0x999';
Engine.context.PermissionController.getCaveat.mockReturnValue({
value: ['0xabc', '0xdef'],
});

expect(() => switchActiveAccounts(hostname, accAddress)).toThrow(
`eth_accounts permission for hostname "${hostname}" does not permit "${accAddress} account".`,
);
});

it('handles duplicates by ensuring uniqueness', () => {
const hostname = 'example.com';
const accAddress = '0xdef';
Engine.context.PermissionController.getCaveat.mockReturnValue({
value: ['0xabc', '0xdef', '0xdef'],
});

switchActiveAccounts(hostname, accAddress);

expect(Engine.context.PermissionController.updateCaveat).toHaveBeenCalledWith(
hostname,
RestrictedMethods.eth_accounts,
CaveatTypes.restrictReturnedAccounts,
['0xdef', '0xabc'],
);
});
});
});
28 changes: 28 additions & 0 deletions app/core/Permissions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import ImportedEngine from '../Engine';
import Logger from '../../util/Logger';
import { getUniqueList } from '../../util/general';
import TransactionTypes from '../TransactionTypes';
import { PermissionKeys } from './specifications';
import { normalizeOrigin } from '../WalletConnect/wc-utils';
import { EVM_IDENTIFIER } from '../Multichain/constants';

const INTERNAL_ORIGINS = [process.env.MM_FOX_CODE, TransactionTypes.MMM];

Expand Down Expand Up @@ -215,3 +218,28 @@ export const getPermittedAccounts = async (
throw error;
}
};

/**
* Get permitted chains for the given the host.
*
* @param hostname - Subject to check if permissions exists. Ex: A Dapp is a subject.
* @returns An array containing permitted chains for the specified host.
*/
export const getPermittedChains = async (hostname: string): Promise<string[]> => {
const { PermissionController } = Engine.context;
const caveat = PermissionController.getCaveat(
normalizeOrigin(hostname),
PermissionKeys.permittedChains,
CaveatTypes.restrictNetworkSwitching
);

if (Array.isArray(caveat?.value)) {
const chains = caveat.value
.filter((item: unknown): item is string => typeof item === 'string' && !isNaN(parseInt(item)))
.map((chainId: string) => `${EVM_IDENTIFIER}:${parseInt(chainId)}`);

return chains;
}

return [];
};
2 changes: 1 addition & 1 deletion app/core/WalletConnect/WalletConnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import NotificationManager from '../NotificationManager';
import { msBetweenDates, msToHours } from '../../util/date';
import { addTransaction } from '../../util/transaction-controller';
import URL from 'url-parse';
import parseWalletConnectUri from './wc-utils';
import { parseWalletConnectUri } from './wc-utils';
import { store } from '../../store';
import { selectChainId } from '../../selectors/networkController';
import ppomUtil from '../../../app/lib/ppom/ppom-util';
Expand Down
Loading
Loading