Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
60739fe
add singleton to env entrypoint
jiexi Feb 3, 2026
bda1283
remove window.mmsdk setting
jiexi Feb 3, 2026
f69f245
WIP
jiexi Feb 4, 2026
0ac2d13
Merge branch 'main' into jl/singleton
jiexi Feb 4, 2026
bc0e33f
make MultichainClient.connect additive
jiexi Feb 4, 2026
102720b
Fix multichain disconnect status bug
jiexi Feb 4, 2026
1f95a07
Leave comment for multichainApiWrapper listener bug
jiexi Feb 4, 2026
53107f6
emitSessionChanged on connect in multichainApiWrapper
jiexi Feb 4, 2026
55ec768
remove activeProviderStorage
jiexi Feb 4, 2026
899174d
fix MulltichainApiClientWrapper transport notification listener handling
jiexi Feb 5, 2026
d16faae
WIP attentuated disconnect
jiexi Feb 5, 2026
790ea15
Update browser playground card disconnect buttons
jiexi Feb 5, 2026
92bf708
actually make wallet_revokeSession call in MWP
jiexi Feb 5, 2026
8ea5c4b
remove old comment
jiexi Feb 5, 2026
b149bec
attempt to resolve eth_accounts and chainId in connect evm session ch…
jiexi Feb 5, 2026
b24d186
fix fulfilled-request log
jiexi Feb 5, 2026
a059171
re-enable removing notification handler in connect evm disconnect
jiexi Feb 5, 2026
eb634d2
update onConnect listener comment
jiexi Feb 5, 2026
10ca0c4
fix typo in method name for wallet_getSession call in DefaultTransport
jiexi Feb 6, 2026
54e38b9
Fix multichain scope cards by not using the onNotification param list…
jiexi Feb 6, 2026
fc3b322
Fix browser playground disconnect all by not guarding on the connecte…
jiexi Feb 6, 2026
aa40223
Disable wallet_revokeSession call inside DefaultTransport's connect()
jiexi Feb 6, 2026
812faaa
remove transport.onNotification
jiexi Feb 6, 2026
6c00cf6
Add merging partials
jiexi Feb 6, 2026
9a6b483
Revert "remove transport.onNotification"
jiexi Feb 7, 2026
237dacf
Merge branch 'main' into jl/singleton
jiexi Feb 9, 2026
23f4230
lint
jiexi Feb 9, 2026
6521750
use parseScopeString instead of startsWith
jiexi Feb 9, 2026
0faca2a
only emit accountsChanged if accounts have actually changed
jiexi Feb 9, 2026
d73e7e7
base Mergeable types on actual types
jiexi Feb 9, 2026
52342e7
remove wallet_revokeSession comment from DefaultTransport.connect
jiexi Feb 9, 2026
9338d19
cleanup
jiexi Feb 9, 2026
48339b5
add #getCaipSession
jiexi Feb 9, 2026
c93774f
remove fix type comment
jiexi Feb 9, 2026
37d37a2
lint
jiexi Feb 9, 2026
4c148a4
DRY singleton
jiexi Feb 9, 2026
eca8f41
lint
jiexi Feb 9, 2026
c1b7a13
fix typo
jiexi Feb 9, 2026
91e31b0
Clear accounts and chain cache
jiexi Feb 9, 2026
e520311
add mergeRequestedSessionWithExisting
jiexi Feb 9, 2026
13c8750
Rearrange MWP disconnect logic
jiexi Feb 10, 2026
319c141
Fix multichain getCaipSession
jiexi Feb 10, 2026
62d2948
Update packages/connect-evm/src/connect.ts
jiexi Feb 10, 2026
7a719a2
fix initial sdkInstance status in browser playground
jiexi Feb 10, 2026
15b0dfc
Merge remote-tracking branch 'origin/jl/singleton' into jl/singleton
jiexi Feb 10, 2026
4d49746
Remove Multichain disconnect in browser playground
jiexi Feb 10, 2026
9c489b5
Fix test-dapp playground
jiexi Feb 10, 2026
fe8233e
remove unnecessary eslint disable
adonesky1 Feb 11, 2026
adf0ec2
Merge branch 'main' into jl/singleton
adonesky1 Feb 11, 2026
f0b569b
remove redundant binding
adonesky1 Feb 11, 2026
09bc769
ensure we're connected before trying to resolve eth_accounts
jiexi Feb 11, 2026
f930bf7
Fix incorrect disconnected state in connect-evm
jiexi Feb 11, 2026
9b83ba3
Merge branch 'main' into jl/singleton
jiexi Feb 11, 2026
e72c3ea
Merge remote-tracking branch 'origin/jl/singleton' into jl/singleton
jiexi Feb 11, 2026
ebaddf8
Merge branch 'main' into jl/singleton
jiexi Feb 12, 2026
b43e5ae
bring in changes from playground branch
jiexi Feb 17, 2026
164e6a9
lint
jiexi Feb 17, 2026
f60c76f
Apply suggestion from @ffmcgee725
jiexi Feb 17, 2026
f45468c
get rid of typecast
jiexi Feb 17, 2026
aac8df1
Merge remote-tracking branch 'origin/jl/singleton' into jl/singleton
jiexi Feb 17, 2026
1426333
unset global singleton if throws
jiexi Feb 17, 2026
89e45a6
use optional chaining for merging options
jiexi Feb 17, 2026
d9a9735
add option merging spec
jiexi Feb 17, 2026
d04c977
exclude dapp and analytics for mergable type
jiexi Feb 17, 2026
54e1774
update mergeOptions spec
jiexi Feb 17, 2026
168d7c2
move sessionChangedHandler into method
jiexi Feb 17, 2026
45faffa
lint
jiexi Feb 17, 2026
e216393
WIP test
jiexi Feb 17, 2026
e5d73be
fix sessionChanged test
jiexi Feb 17, 2026
ec18eb4
add EvmClient.disconnect() spec
jiexi Feb 17, 2026
0b0ae78
add connect() test
jiexi Feb 18, 2026
8fd54f6
lint
jiexi Feb 18, 2026
e34d764
Add openConnectDeeplinkIfNeeded
jiexi Feb 18, 2026
2116ed8
Add missing removedStoredSessionRequest
jiexi Feb 18, 2026
72f7a30
make multichain connect button gray when connecting
jiexi Feb 18, 2026
0203c52
Make SDKProvider start in connecting state
jiexi Feb 18, 2026
ec72dde
Fix resumeTimeout on refresh
jiexi Feb 18, 2026
5b9df60
clear storedSessionRequest after connection handled
jiexi Feb 18, 2026
22ec40d
remove refresh timeout fix
jiexi Feb 18, 2026
69375df
Revert "Add missing removedStoredSessionRequest"
jiexi Feb 18, 2026
1ae0d2c
Revert "clear storedSessionRequest after connection handled"
jiexi Feb 18, 2026
32d0e75
Revert "Add openConnectDeeplinkIfNeeded"
jiexi Feb 18, 2026
aad3e60
restore throw error on connect if already connecting
jiexi Feb 18, 2026
3f3cdb5
lint
jiexi Feb 18, 2026
25b3ef0
build prereq packages in connect-evm
jiexi Feb 18, 2026
241713c
fix missing api options in spec
jiexi Feb 18, 2026
bcfbd54
fix session test
jiexi Feb 18, 2026
b9087d5
Fix singleton test reset
jiexi Feb 18, 2026
a99c7b9
fix handle disconnect error spec
jiexi Feb 19, 2026
4ab38c3
Fix last connect test
jiexi Feb 19, 2026
7b0f15c
changelog
jiexi Feb 18, 2026
0281b2d
lint
jiexi Feb 19, 2026
f20b443
lint
jiexi Feb 19, 2026
8d0dd51
lint
jiexi Feb 19, 2026
32f67df
lint
jiexi Feb 19, 2026
7a74583
fix typo setupTransportNotifcationListener
jiexi Feb 19, 2026
f3ba9ce
fix typo clearTransportNotifcationListener
jiexi Feb 19, 2026
abb2d68
rename to mergedScopes/CaipAccountsIds/SessionProperties
jiexi Feb 19, 2026
64a78f8
use mergedScopes and caipAccountIds for deeplink initiated MWP
jiexi Feb 19, 2026
638a938
Update packages/connect-multichain/src/multichain/transports/multicha…
jiexi Feb 20, 2026
7441fbc
Fix connect evm status
jiexi Feb 20, 2026
d708c76
Merge branch 'main' into jl/singleton
jiexi Feb 20, 2026
cee7a7c
Merge remote-tracking branch 'origin/jl/singleton' into jl/singleton
jiexi Feb 20, 2026
aa366a5
add emitSessionChanged comments
jiexi Feb 20, 2026
d31249d
add comment to create()
jiexi Feb 20, 2026
bef3c80
elaborate on which params are not merged
jiexi Feb 20, 2026
5d4b50a
clean up sendEip1193message eth_accounts cast
jiexi Feb 20, 2026
c4c01f3
Merge branch 'main' into jl/singleton
jiexi Feb 20, 2026
d514b7d
Merge branch 'main' into jl/singleton
jiexi Feb 20, 2026
d889a4b
Merge branch 'main' into jl/singleton
jiexi Feb 20, 2026
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
1 change: 1 addition & 0 deletions packages/connect-evm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `debug` option param used by `createEVMClient()` now enables console debug logs of the underlying `MultichainClient` instance ([#149](https://github.com/MetaMask/connect-monorepo/pull/149))
- update `connect()` and `createEVMClient()` typings to be more accurate ([#153](https://github.com/MetaMask/connect-monorepo/pull/153))
- update `switchChain()` to return `Promise<void>` ([#153](https://github.com/MetaMask/connect-monorepo/pull/153))
- Make `ConnectEvm` rely on `wallet_sessionChanged` events from `ConnectMultichain` rather than explicit connect/disconnect events ([#157](https://github.com/MetaMask/connect-monorepo/pull/157))

### Fixed

Expand Down
5 changes: 3 additions & 2 deletions packages/connect-evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
"dev": "tsup --watch ",
"publish:preview": "yarn npm publish --tag preview",
"since-latest-release": "../../scripts/since-latest-release.sh",
"test": "vitest run",
"test:ci": "vitest run --coverage --coverage.reporter=text --silent",
"pretest": "yarn workspace @metamask/analytics run build && yarn workspace @metamask/multichain-ui run build && yarn workspace @metamask/connect-multichain run build",
"test": "yarn pretest && vitest run",
"test:ci": "yarn pretest && vitest run --coverage --coverage.reporter=text --silent",
"test:unit": "vitest run",
"test:verbose": "vitest run --reporter=verbose",
"test:watch": "vitest watch"
Expand Down
348 changes: 344 additions & 4 deletions packages/connect-evm/src/connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,348 @@
/* eslint-disable @typescript-eslint/no-shadow -- Vitest globals */
import { describe, it, expect } from 'vitest';
import type { SessionData, MultichainCore } from '@metamask/connect-multichain';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';

describe('smoke', () => {
it('works', () => {
expect(true).toBe(true);
import type { ConnectEvmStatus } from './connect';
import { MetamaskConnectEVM } from './connect';

type MockCore = MultichainCore & {
emit: (event: string, ...args: unknown[]) => void;
_status: ConnectEvmStatus;
storage: MultichainCore['storage'] & {
adapter: {
get: Mock<(key: string) => Promise<string | null>>;
set: Mock<(key: string, value: string) => Promise<void>>;
};
};
transport: MultichainCore['transport'] & {
sendEip1193Message: Mock;
};
disconnect: Mock<(scopes?: unknown[]) => Promise<void>>;
connect: Mock<
(
scopes: unknown[],
caipAccountIds: unknown[],
sessionProperties?: unknown,
forceRequest?: boolean,
) => Promise<void>
>;
};

/**
* Creates a mock MultichainCore for testing.
*
* @returns A mock core instance implementing MockCore.
*/
function createMockCore(): MockCore {
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
const _status: ConnectEvmStatus = 'disconnected';

const sendEip1193Message = vi.fn().mockResolvedValue({
result: [] as string[],
id: 1,
jsonrpc: '2.0' as const,
});
const onNotification = vi.fn().mockReturnValue(() => {
// noop
});

const storageGet = vi.fn().mockResolvedValue(null);
const storageSet = vi.fn().mockResolvedValue(undefined);

const mockCore = {
// eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status
_status: _status as ConnectEvmStatus,
get status(): ConnectEvmStatus {
return this._status;
},
set status(value: ConnectEvmStatus) {
this._status = value;
},
on(event: string, handler: (...args: unknown[]) => void): void {
if (!handlers[event]) {
handlers[event] = [];
}
handlers[event].push(handler);
},
emit(event: string, ...args: unknown[]): void {
handlers[event]?.forEach((handler) => handler(...args));
},
emitSessionChanged: vi.fn().mockImplementation(async (): Promise<void> => {
mockCore.emit('wallet_sessionChanged', { sessionScopes: {} });
}),
disconnect: vi.fn().mockResolvedValue(undefined),
connect: vi.fn().mockResolvedValue(undefined),
transport: {
sendEip1193Message,
onNotification,
},
storage: {
adapter: {
get: storageGet,
set: storageSet,
},
},
};

mockCore._status = _status;
return mockCore as unknown as MockCore;
}

describe('MetamaskConnectEVM', () => {
describe('#onSessionChanged', () => {
describe('disconnects', () => {
let mockCore: MockCore;
let client: Awaited<ReturnType<typeof MetamaskConnectEVM.create>>;

beforeEach(async () => {
mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1'));
client = await MetamaskConnectEVM.create({ core: mockCore });
const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
},
};
mockCore.emit('wallet_sessionChanged', session);
await new Promise<void>((resolve) => {
client.getProvider().once('connect', () => resolve());
});
});

it('disconnects when session has no permitted EIP-155 chain IDs if the MultichainClient is connected', async () => {
const disconnectPromise = new Promise<void>((resolve) => {
client.getProvider().once('disconnect', resolve);
});

const newSession: SessionData = {
sessionScopes: {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': {
methods: [],
notifications: [],
accounts: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:1234567890'],
},
},
};

mockCore.emit('wallet_sessionChanged', newSession);
await disconnectPromise;
expect(client.accounts).toEqual([]);
});

it('disconnects when wallet_sessionChanged is emitted with undefined session after being connected', async () => {
const disconnectPromise = new Promise<void>((resolve) => {
client.getProvider().once('disconnect', resolve);
});
mockCore.emit('wallet_sessionChanged', undefined);
await disconnectPromise;
expect(client.accounts).toEqual([]);
});

it('disconnects when wallet_sessionChanged is emitted with empty sessionScopes after being connected', async () => {
const disconnectPromise = new Promise<void>((resolve) => {
client.getProvider().once('disconnect', resolve);
});
mockCore.emit('wallet_sessionChanged', { sessionScopes: {} });
await disconnectPromise;
expect(client.accounts).toEqual([]);
});
});

describe('connects', () => {
it('connects using the accounts from the CAIP-25 permissions when the MultichainClient is disconnected', async () => {
const mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1'));
const client = await MetamaskConnectEVM.create({ core: mockCore });

const connectPromise = new Promise<{
chainId: string;
accounts: string[];
}>((resolve) => {
client.getProvider().once('connect', resolve);
});

const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
},
};
mockCore.emit('wallet_sessionChanged', session);

const connectData = await connectPromise;
expect(connectData.chainId).toBe('0x1');
expect(connectData.accounts).toContain(
'0x1234567890123456789012345678901234567890',
);
});

it('connects using accounts from a eth_accounts response when the MultichainClient is connected', async () => {
const mockCore = createMockCore();
mockCore._status = 'connected';
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1'));
mockCore.transport.sendEip1193Message.mockResolvedValue({
result: ['0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'],
id: 1,
jsonrpc: '2.0',
});

const client = await MetamaskConnectEVM.create({ core: mockCore });

const connectPromise = new Promise<{
chainId: string;
accounts: string[];
}>((resolve) => {
client.getProvider().once('connect', resolve);
});

const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
},
};
mockCore.emit('wallet_sessionChanged', session);

const connectData = await connectPromise;
expect(connectData.chainId).toBe('0x1');
expect(connectData.accounts).toContain(
'0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
);
expect(mockCore.transport.sendEip1193Message).toHaveBeenCalledWith({
method: 'eth_accounts',
params: [],
});
});

it('connects using the cached eth_chainId when valid and also in the CAIP-25 permission scopes', async () => {
const mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x89')); // Polygon
const client = await MetamaskConnectEVM.create({ core: mockCore });

const connectPromise = new Promise<{ chainId: string }>((resolve) => {
client.getProvider().once('connect', (data) => resolve(data));
});

const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
'eip155:137': {
methods: [],
notifications: [],
accounts: [
'eip155:137:0x1234567890123456789012345678901234567890',
],
},
},
};
mockCore.emit('wallet_sessionChanged', session);

const connectData = await connectPromise;
expect(connectData.chainId).toBe('0x89');
});

it('connects using the first permitted chain id from the CAIP-25 permission when there is no cached eth_chainId', async () => {
const mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(null);
const client = await MetamaskConnectEVM.create({ core: mockCore });

const connectPromise = new Promise<{ chainId: string }>((resolve) => {
client.getProvider().once('connect', (data) => resolve(data));
});

const session: SessionData = {
sessionScopes: {
'eip155:11155111': {
methods: [],
notifications: [],
accounts: [
'eip155:11155111:0x1234567890123456789012345678901234567890',
],
},
},
};
mockCore.emit('wallet_sessionChanged', session);

const connectData = await connectPromise;
expect(connectData.chainId).toBe('0xaa36a7'); // sepolia
});
});
});

describe('connect', () => {
it('resolves with the value emitted by the provider connect event (triggered by wallet_sessionChanged)', async () => {
const mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1'));
mockCore.connect.mockImplementation(async (): Promise<void> => {
const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
},
};
mockCore.emit('wallet_sessionChanged', session);
});
const client = await MetamaskConnectEVM.create({ core: mockCore });

const result = await client.connect({ chainIds: ['0x1'] });

expect(result).toEqual({
chainId: '0x1',
accounts: ['0x1234567890123456789012345678901234567890'],
});
});
});

describe('disconnect', () => {
it('calls core.disconnect with all eip155 scopes from the current session', async () => {
const mockCore = createMockCore();
mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1'));
const client = await MetamaskConnectEVM.create({ core: mockCore });

const session: SessionData = {
sessionScopes: {
'eip155:1': {
methods: [],
notifications: [],
accounts: ['eip155:1:0x1234567890123456789012345678901234567890'],
},
'eip155:137': {
methods: [],
notifications: [],
accounts: ['eip155:137:0x1234567890123456789012345678901234567890'],
},
},
};
mockCore.emit('wallet_sessionChanged', session);
await new Promise<void>((resolve) => {
client.getProvider().once('connect', () => resolve());
});

await client.disconnect();

expect(mockCore.disconnect).toHaveBeenCalledTimes(1);
const [scopes] = mockCore.disconnect.mock.calls[0];
expect(scopes).toEqual(
expect.arrayContaining(['eip155:1', 'eip155:137']),
);
expect(scopes).toHaveLength(2);
});
});
});
Loading
Loading