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 10 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
27 changes: 27 additions & 0 deletions app/core/Permissions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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';

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

Expand Down Expand Up @@ -215,3 +217,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')
.map((chainId: string) => `eip155:${parseInt(chainId)}`);

return chains;
}

return [];
};
139 changes: 89 additions & 50 deletions app/core/WalletConnect/WalletConnect2Session.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import WalletConnect2Session from './WalletConnect2Session';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Ignoring the import error for testing purposes
import { Client } from '@walletconnect/se-sdk';
import { NavigationContainerRef } from '@react-navigation/native';
import { IWalletKit } from '@reown/walletkit';
import { SessionTypes } from '@walletconnect/types';
import { store } from '../../store';
import Engine from '../Engine';
Expand All @@ -19,19 +17,31 @@ jest.mock('../AppConstants', () => ({
},
},
BUNDLE_IDS: {
ANDROID: 'com.test.app', // Make sure this is correctly mocked
ANDROID: 'com.test.app',
},
}));

jest.mock('@walletconnect/se-sdk', () => ({
Client: jest.fn().mockImplementation(() => ({
jest.mock('@reown/walletkit', () => {
const mockClient = {
approveRequest: jest.fn(),
rejectRequest: jest.fn(),
updateSession: jest.fn(),
getPendingSessionRequests: jest.fn(),
respondSessionRequest: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
})),
}));
};

return {
__esModule: true,
default: {
init: jest.fn().mockResolvedValue(mockClient),
},
WalletKit: {
init: jest.fn().mockResolvedValue(mockClient),
},
Client: jest.fn().mockImplementation(() => mockClient),
};
});

jest.mock('../BackgroundBridge/BackgroundBridge', () =>
jest.fn().mockImplementation(() => ({
Expand All @@ -46,26 +56,42 @@ jest.mock('../Engine');
jest.mock('../SDKConnect/utils/DevLogger', () => ({
log: jest.fn(),
}));
jest.mock('../Permissions');
jest.mock('../Permissions', () => ({
getPermittedAccounts: jest.fn().mockResolvedValue(['0x1234567890abcdef1234567890abcdef12345678']),
getPermittedChains: jest.fn().mockResolvedValue(['eip155:1']),
}));
jest.mock('../../store', () => ({
store: {
getState: jest.fn(),
},
}));
jest.mock('../RPCMethods/RPCMethodMiddleware');
jest.mock('../RPCMethods/RPCMethodMiddleware', () => ({
__esModule: true,
default: () => () => ({ acknowledged: () => Promise.resolve() }),
}));
jest.mock('./wc-utils', () => ({
hideWCLoadingState: jest.fn(),
showWCLoadingState: jest.fn(),
checkWCPermissions: jest.fn().mockResolvedValue(true),
getScopedPermissions: jest.fn().mockResolvedValue({
eip155: {
chains: ['eip155:1'],
methods: ['eth_sendTransaction'],
events: ['chainChanged', 'accountsChanged'],
accounts: ['eip155:1:0x1234567890abcdef1234567890abcdef12345678']
}
}),
normalizeOrigin: jest.fn().mockImplementation((url) => url),
}));

describe('WalletConnect2Session', () => {
let session: WalletConnect2Session;
let mockClient: Client;
let mockClient: IWalletKit;
let mockSession: SessionTypes.Struct;
let mockNavigation: NavigationContainerRef;

beforeEach(() => {
mockClient = new Client();
mockClient = new (jest.requireMock('@reown/walletkit').Client)();
mockSession = {
topic: 'test-topic',
pairingTopic: 'test-pairing',
Expand All @@ -75,14 +101,12 @@ describe('WalletConnect2Session', () => {
} as unknown as SessionTypes.Struct;
mockNavigation = {} as NavigationContainerRef;

// Mock store state
(store.getState as jest.Mock).mockReturnValue({
inpageProvider: {
networkId: '1',
},
});

// Mock Engine.context
Object.defineProperty(Engine, 'context', {
value: {
AccountsController: {
Expand All @@ -98,8 +122,7 @@ describe('WalletConnect2Session', () => {
getNetworkClientById: jest.fn().mockReturnValue({ chainId: '0x2' }),
},
PermissionController: {
// eslint-disable-next-line no-empty-function
createPermissionMiddleware: jest.fn().mockReturnValue(() => {}),
createPermissionMiddleware: jest.fn().mockReturnValue(() => ({ result: true })),
},
},
writable: true,
Expand All @@ -113,8 +136,6 @@ describe('WalletConnect2Session', () => {
navigation: mockNavigation,
});

// Manually set the topicByRequestId to ensure it's populated correctly for tests
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(session as any).topicByRequestId = { '1': mockSession.topic };
});

Expand All @@ -131,9 +152,10 @@ describe('WalletConnect2Session', () => {
});

it('should handle request correctly and reject invalid chainId', async () => {
const mockRejectRequest = jest
.spyOn(mockClient, 'rejectRequest')
.mockResolvedValue(undefined);
const mockRespondSessionRequest = jest
.spyOn(mockClient, 'respondSessionRequest')
.mockImplementation(async () => { /* empty implementation */ });

const requestEvent = {
id: '1',
topic: 'test-topic',
Expand All @@ -144,21 +166,25 @@ describe('WalletConnect2Session', () => {
params: [],
},
},
verifyContext: {},
verifyContext: {
verified: {
origin: 'https://example.com'
}
},
};

(store.getState as jest.Mock).mockReturnValue({
inpageProvider: {
networkId: '1',
},
});
const { checkWCPermissions } = jest.requireMock('./wc-utils');
checkWCPermissions.mockResolvedValueOnce(false);

await session.handleRequest(requestEvent as any);

expect(mockRejectRequest).toHaveBeenCalledWith({
id: '1',
expect(mockRespondSessionRequest).toHaveBeenCalledWith({
topic: mockSession.topic,
error: { code: 1, message: 'Invalid chainId' },
response: {
id: '1',
jsonrpc: '2.0',
error: { code: 1, message: 'Invalid chainId' },
},
});
});

Expand All @@ -174,53 +200,66 @@ describe('WalletConnect2Session', () => {
});

it('should approve a request correctly', async () => {
const mockApproveRequest = jest
.spyOn(mockClient, 'approveRequest')
const mockRespondSessionRequest = jest
.spyOn(mockClient, 'respondSessionRequest')
.mockResolvedValue(undefined);
const request = { id: '1', result: '0x123' };

await session.approveRequest(request);

expect(mockApproveRequest).toHaveBeenCalledWith({
id: parseInt(request.id),
expect(mockRespondSessionRequest).toHaveBeenCalledWith({
topic: mockSession.topic,
result: request.result,
response: {
id: 1,
jsonrpc: '2.0',
result: request.result,
},
});
});

it('should reject a request correctly', async () => {
const mockRejectRequest = jest
.spyOn(mockClient, 'rejectRequest')
const mockRespondSessionRequest = jest
.spyOn(mockClient, 'respondSessionRequest')
.mockResolvedValue(undefined);
const request = { id: '1', error: new Error('User rejected') };

await session.rejectRequest(request);

expect(mockRejectRequest).toHaveBeenCalledWith({
id: parseInt(request.id),
expect(mockRespondSessionRequest).toHaveBeenCalledWith({
topic: mockSession.topic,
error: { code: 5000, message: 'User rejected' },
response: {
id: 1,
jsonrpc: '2.0',
error: { code: 5000, message: 'User rejected' },
},
});
});

it('should handle session update correctly', async () => {
const mockUpdateSession = jest
.spyOn(mockClient, 'updateSession')
.mockResolvedValue(undefined);
const approvedAccounts = ['0x123'];
.mockResolvedValue({ acknowledged: () => Promise.resolve() });
const accounts = ['0x123'];
const chainId = 1;

// Mock the getApprovedAccountsFromPermissions method
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn(session as any, 'getApprovedAccountsFromPermissions')
.mockResolvedValue(approvedAccounts);
(store.getState as jest.Mock).mockReturnValue({
inpageProvider: {
networkId: '1',
},
});

await session.updateSession({ chainId: 1, accounts: approvedAccounts });
await session.updateSession({ chainId, accounts });

expect(mockUpdateSession).toHaveBeenCalledWith({
topic: mockSession.topic,
chainId: 1,
accounts: approvedAccounts,
namespaces: {
eip155: {
chains: ['eip155:1'],
methods: ['eth_sendTransaction'],
events: ['chainChanged', 'accountsChanged'],
accounts: ['eip155:1:0x1234567890abcdef1234567890abcdef12345678']
}
}
});
});
});
Loading
Loading