Skip to content

Commit ce8c429

Browse files
authored
fix: use randomly generated request id in windowPostMessageTransport and externallyConnectableTransport (#81)
* fix: use randomly generated request id in windowPostMessageTransport and externallyConnectableTransport
1 parent 6f27605 commit ce8c429

File tree

6 files changed

+56
-19
lines changed

6 files changed

+56
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- fix: use randomly generated request id on `windowPostMessageTransport` and `externallyConnectableTransport` to avoid conflicts across disconnect/reconnect cycles in firefox ([#81](https://github.com/MetaMask/multichain-api-client/pull/81))
13+
1014
## [0.8.0]
1115

1216
### Added

src/helpers/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
// chrome is a global injected by browser extensions
22
declare const chrome: any;
33

4+
// uint32 (two's complement) max
5+
// more conservative than Number.MAX_SAFE_INTEGER
6+
const MAX = 4_294_967_295;
7+
let idCounter = Math.floor(Math.random() * MAX);
8+
9+
/**
10+
* Gets an ID that is guaranteed to be unique so long as no more than
11+
* 4_294_967_295 (uint32 max) IDs are created, or the IDs are rapidly turned
12+
* over.
13+
*
14+
* @returns The unique ID.
15+
*/
16+
export const getUniqueId = (): number => {
17+
idCounter = (idCounter + 1) % MAX;
18+
return idCounter;
19+
};
20+
421
/**
522
* Detects if we're in a Chrome-like environment with extension support
623
*/

src/transports/externallyConnectableTransport.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { type MockPort, mockSession } from '../../tests/mocks';
33
import * as metamaskExtensionId from '../helpers/metamaskExtensionId';
4+
import * as utils from '../helpers/utils';
45
import { TransportError } from '../types/errors';
56
import { getExternallyConnectableTransport } from './externallyConnectableTransport';
67

@@ -19,11 +20,18 @@ describe('ExternallyConnectableTransport', () => {
1920
let transport: ReturnType<typeof getExternallyConnectableTransport>;
2021
let messageHandler: (msg: any) => void;
2122
let disconnectHandler: () => void;
23+
const MOCK_INITIAL_REQUEST_ID = 1000;
2224

2325
let mockPort: MockPort;
2426
beforeEach(() => {
2527
vi.clearAllMocks();
2628

29+
// Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID
30+
let requestIdCounter = MOCK_INITIAL_REQUEST_ID;
31+
vi.spyOn(utils, 'getUniqueId').mockImplementation(() => {
32+
return requestIdCounter++;
33+
});
34+
2735
// Setup mock port
2836
mockPort = {
2937
postMessage: vi.fn(),
@@ -80,7 +88,7 @@ describe('ExternallyConnectableTransport', () => {
8088
messageHandler({
8189
type: 'caip-348',
8290
data: {
83-
id: 1,
91+
id: MOCK_INITIAL_REQUEST_ID,
8492
jsonrpc: '2.0',
8593
result: mockSession,
8694
},
@@ -91,7 +99,7 @@ describe('ExternallyConnectableTransport', () => {
9199
expect(mockPort.postMessage).toHaveBeenCalledWith({
92100
type: 'caip-348',
93101
data: {
94-
id: 1,
102+
id: MOCK_INITIAL_REQUEST_ID,
95103
jsonrpc: '2.0',
96104
method: 'wallet_getSession',
97105
},
@@ -156,19 +164,19 @@ describe('ExternallyConnectableTransport', () => {
156164
'Transport request timed out',
157165
);
158166

159-
// Second request should work (id 2)
167+
// Second request should work (id MOCK_INITIAL_REQUEST_ID + 1)
160168
const secondPromise = transport.request({ method: 'wallet_getSession' });
161169

162170
messageHandler({
163171
type: 'caip-348',
164172
data: {
165-
id: 2,
173+
id: MOCK_INITIAL_REQUEST_ID + 1,
166174
jsonrpc: '2.0',
167175
result: mockSession,
168176
},
169177
});
170178

171179
const response = await secondPromise;
172-
expect(response).toEqual({ id: 2, jsonrpc: '2.0', result: mockSession });
180+
expect(response).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, jsonrpc: '2.0', result: mockSession });
173181
});
174182
});

src/transports/externallyConnectableTransport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { detectMetamaskExtensionId } from '../helpers/metamaskExtensionId';
2-
import { withTimeout } from '../helpers/utils';
2+
import { getUniqueId, withTimeout } from '../helpers/utils';
33
import { TransportError, TransportTimeoutError } from '../types/errors';
44
import type { Transport, TransportResponse } from '../types/transport';
55
import { DEFAULT_REQUEST_TIMEOUT, REQUEST_CAIP } from './constants';
@@ -28,7 +28,7 @@ export function getExternallyConnectableTransport(
2828
let { extensionId } = params;
2929
const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params;
3030
let chromePort: chrome.runtime.Port | undefined;
31-
let requestId = 1;
31+
let requestId = getUniqueId();
3232
const pendingRequests = new Map<number, (value: any) => void>();
3333

3434
/**

src/transports/windowPostMessageTransport.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest';
22
import { mockSession } from '../../tests/mocks';
3+
import * as utils from '../helpers/utils';
34
import { TransportError } from '../types/errors';
45
import { CONTENT_SCRIPT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants';
56
import { getWindowPostMessageTransport } from './windowPostMessageTransport';
@@ -24,11 +25,18 @@ global.location = mockLocation as any;
2425
describe('WindowPostMessageTransport', () => {
2526
let transport: ReturnType<typeof getWindowPostMessageTransport>;
2627
let messageHandler: (event: MessageEvent) => void;
28+
const MOCK_INITIAL_REQUEST_ID = 1000;
2729

2830
beforeEach(() => {
2931
// Reset mocks
3032
vi.clearAllMocks();
3133

34+
// Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID
35+
let requestIdCounter = MOCK_INITIAL_REQUEST_ID;
36+
vi.spyOn(utils, 'getUniqueId').mockImplementation(() => {
37+
return requestIdCounter++;
38+
});
39+
3240
// Setup addEventListener mock to capture the message handler
3341
mockWindow.addEventListener.mockImplementation((event: string, handler: (event: MessageEvent) => void) => {
3442
if (event === 'message') {
@@ -62,7 +70,7 @@ describe('WindowPostMessageTransport', () => {
6270
name: MULTICHAIN_SUBSTREAM_NAME,
6371
data: {
6472
jsonrpc: '2.0',
65-
id: 1,
73+
id: MOCK_INITIAL_REQUEST_ID,
6674
method: 'wallet_getSession',
6775
},
6876
},
@@ -77,7 +85,7 @@ describe('WindowPostMessageTransport', () => {
7785
data: {
7886
name: MULTICHAIN_SUBSTREAM_NAME,
7987
data: {
80-
id: 1,
88+
id: MOCK_INITIAL_REQUEST_ID,
8189
result: mockSession,
8290
},
8391
},
@@ -87,7 +95,7 @@ describe('WindowPostMessageTransport', () => {
8795

8896
const response = await requestPromise;
8997
expect(response).toEqual({
90-
id: 1,
98+
id: MOCK_INITIAL_REQUEST_ID,
9199
result: mockSession,
92100
});
93101
});
@@ -260,7 +268,7 @@ describe('WindowPostMessageTransport', () => {
260268
data: {
261269
name: MULTICHAIN_SUBSTREAM_NAME,
262270
data: {
263-
id: 2,
271+
id: MOCK_INITIAL_REQUEST_ID + 1,
264272
result: { success: true },
265273
},
266274
},
@@ -274,7 +282,7 @@ describe('WindowPostMessageTransport', () => {
274282
data: {
275283
name: MULTICHAIN_SUBSTREAM_NAME,
276284
data: {
277-
id: 1,
285+
id: MOCK_INITIAL_REQUEST_ID,
278286
result: mockSession,
279287
},
280288
},
@@ -284,11 +292,11 @@ describe('WindowPostMessageTransport', () => {
284292

285293
const [response1, response2] = await Promise.all([request1Promise, request2Promise]);
286294
expect(response1).toEqual({
287-
id: 1,
295+
id: MOCK_INITIAL_REQUEST_ID,
288296
result: mockSession,
289297
});
290298
expect(response2).toEqual({
291-
id: 2,
299+
id: MOCK_INITIAL_REQUEST_ID + 1,
292300
result: { success: true },
293301
});
294302
});
@@ -324,14 +332,14 @@ describe('WindowPostMessageTransport', () => {
324332
// Second request should still work (simulate response)
325333
const secondPromise = transport.request({ method: 'wallet_getSession' });
326334

327-
// Simulate response for id 2 (because first timed out with id 1, second increments to 2)
335+
// Simulate response for id MOCK_INITIAL_REQUEST_ID + 1 (because first timed out with id MOCK_INITIAL_REQUEST_ID, second increments to MOCK_INITIAL_REQUEST_ID + 1)
328336
messageHandler({
329337
data: {
330338
target: INPAGE,
331339
data: {
332340
name: MULTICHAIN_SUBSTREAM_NAME,
333341
data: {
334-
id: 2,
342+
id: MOCK_INITIAL_REQUEST_ID + 1,
335343
result: mockSession,
336344
},
337345
},
@@ -340,6 +348,6 @@ describe('WindowPostMessageTransport', () => {
340348
} as MessageEvent);
341349

342350
const result = await secondPromise;
343-
expect(result).toEqual({ id: 2, result: mockSession });
351+
expect(result).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, result: mockSession });
344352
});
345353
});

src/transports/windowPostMessageTransport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { withTimeout } from '../helpers/utils';
1+
import { getUniqueId, withTimeout } from '../helpers/utils';
22
import { TransportError, TransportTimeoutError } from '../types/errors';
33
import type { Transport, TransportResponse } from '../types/transport';
44
import { CONTENT_SCRIPT, DEFAULT_REQUEST_TIMEOUT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants';
@@ -20,7 +20,7 @@ export function getWindowPostMessageTransport(params: { defaultTimeout?: number
2020
const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params;
2121
let messageListener: ((event: MessageEvent) => void) | null = null;
2222
const pendingRequests: Map<number, (value: any) => void> = new Map();
23-
let requestId = 1;
23+
let requestId = getUniqueId();
2424
/**
2525
* Storing notification callbacks.
2626
* If we detect a "notification" (a message without an id) coming from the extension, we'll call each callback in here.

0 commit comments

Comments
 (0)