Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- fix: use timestamp based request id on `windowPostMessageTransport` to avoid conflicts across disconnect/reconnect cycles in firefox ([#81](https://github.com/MetaMask/multichain-api-client/pull/81))

## [0.8.0]

### Added
Expand Down
17 changes: 17 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// chrome is a global injected by browser extensions
declare const chrome: any;

// uint32 (two's complement) max
// more conservative than Number.MAX_SAFE_INTEGER
const MAX = 4_294_967_295;
let idCounter = Math.floor(Math.random() * MAX);

/**
* Gets an ID that is guaranteed to be unique so long as no more than
* 4_294_967_295 (uint32 max) IDs are created, or the IDs are rapidly turned
* over.
*
* @returns The unique ID.
*/
export const getUniqueId = (): number => {
idCounter = (idCounter + 1) % MAX;
return idCounter;
};

/**
* Detects if we're in a Chrome-like environment with extension support
*/
Expand Down
18 changes: 13 additions & 5 deletions src/transports/externallyConnectableTransport.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { type MockPort, mockSession } from '../../tests/mocks';
import * as metamaskExtensionId from '../helpers/metamaskExtensionId';
import * as utils from '../helpers/utils';
import { TransportError } from '../types/errors';
import { getExternallyConnectableTransport } from './externallyConnectableTransport';

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

let mockPort: MockPort;
beforeEach(() => {
vi.clearAllMocks();

// Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID
let requestIdCounter = MOCK_INITIAL_REQUEST_ID;
vi.spyOn(utils, 'getUniqueId').mockImplementation(() => {
return requestIdCounter++;
});

// Setup mock port
mockPort = {
postMessage: vi.fn(),
Expand Down Expand Up @@ -80,7 +88,7 @@ describe('ExternallyConnectableTransport', () => {
messageHandler({
type: 'caip-348',
data: {
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
jsonrpc: '2.0',
result: mockSession,
},
Expand All @@ -91,7 +99,7 @@ describe('ExternallyConnectableTransport', () => {
expect(mockPort.postMessage).toHaveBeenCalledWith({
type: 'caip-348',
data: {
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
jsonrpc: '2.0',
method: 'wallet_getSession',
},
Expand Down Expand Up @@ -156,19 +164,19 @@ describe('ExternallyConnectableTransport', () => {
'Transport request timed out',
);

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

messageHandler({
type: 'caip-348',
data: {
id: 2,
id: MOCK_INITIAL_REQUEST_ID + 1,
jsonrpc: '2.0',
result: mockSession,
},
});

const response = await secondPromise;
expect(response).toEqual({ id: 2, jsonrpc: '2.0', result: mockSession });
expect(response).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, jsonrpc: '2.0', result: mockSession });
});
});
4 changes: 2 additions & 2 deletions src/transports/externallyConnectableTransport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { detectMetamaskExtensionId } from '../helpers/metamaskExtensionId';
import { withTimeout } from '../helpers/utils';
import { getUniqueId, withTimeout } from '../helpers/utils';
import { TransportError, TransportTimeoutError } from '../types/errors';
import type { Transport, TransportResponse } from '../types/transport';
import { DEFAULT_REQUEST_TIMEOUT, REQUEST_CAIP } from './constants';
Expand Down Expand Up @@ -28,7 +28,7 @@ export function getExternallyConnectableTransport(
let { extensionId } = params;
const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params;
let chromePort: chrome.runtime.Port | undefined;
let requestId = 1;
let requestId = getUniqueId();
const pendingRequests = new Map<number, (value: any) => void>();

/**
Expand Down
28 changes: 18 additions & 10 deletions src/transports/windowPostMessageTransport.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mockSession } from '../../tests/mocks';
import * as utils from '../helpers/utils';
import { TransportError } from '../types/errors';
import { CONTENT_SCRIPT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants';
import { getWindowPostMessageTransport } from './windowPostMessageTransport';
Expand All @@ -24,11 +25,18 @@ global.location = mockLocation as any;
describe('WindowPostMessageTransport', () => {
let transport: ReturnType<typeof getWindowPostMessageTransport>;
let messageHandler: (event: MessageEvent) => void;
const MOCK_INITIAL_REQUEST_ID = 1000;

beforeEach(() => {
// Reset mocks
vi.clearAllMocks();

// Mock getUniqueId() to return sequential values starting from MOCK_INITIAL_REQUEST_ID
let requestIdCounter = MOCK_INITIAL_REQUEST_ID;
vi.spyOn(utils, 'getUniqueId').mockImplementation(() => {
return requestIdCounter++;
});

// Setup addEventListener mock to capture the message handler
mockWindow.addEventListener.mockImplementation((event: string, handler: (event: MessageEvent) => void) => {
if (event === 'message') {
Expand Down Expand Up @@ -62,7 +70,7 @@ describe('WindowPostMessageTransport', () => {
name: MULTICHAIN_SUBSTREAM_NAME,
data: {
jsonrpc: '2.0',
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
method: 'wallet_getSession',
},
},
Expand All @@ -77,7 +85,7 @@ describe('WindowPostMessageTransport', () => {
data: {
name: MULTICHAIN_SUBSTREAM_NAME,
data: {
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
result: mockSession,
},
},
Expand All @@ -87,7 +95,7 @@ describe('WindowPostMessageTransport', () => {

const response = await requestPromise;
expect(response).toEqual({
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
result: mockSession,
});
});
Expand Down Expand Up @@ -260,7 +268,7 @@ describe('WindowPostMessageTransport', () => {
data: {
name: MULTICHAIN_SUBSTREAM_NAME,
data: {
id: 2,
id: MOCK_INITIAL_REQUEST_ID + 1,
result: { success: true },
},
},
Expand All @@ -274,7 +282,7 @@ describe('WindowPostMessageTransport', () => {
data: {
name: MULTICHAIN_SUBSTREAM_NAME,
data: {
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
result: mockSession,
},
},
Expand All @@ -284,11 +292,11 @@ describe('WindowPostMessageTransport', () => {

const [response1, response2] = await Promise.all([request1Promise, request2Promise]);
expect(response1).toEqual({
id: 1,
id: MOCK_INITIAL_REQUEST_ID,
result: mockSession,
});
expect(response2).toEqual({
id: 2,
id: MOCK_INITIAL_REQUEST_ID + 1,
result: { success: true },
});
});
Expand Down Expand Up @@ -324,14 +332,14 @@ describe('WindowPostMessageTransport', () => {
// Second request should still work (simulate response)
const secondPromise = transport.request({ method: 'wallet_getSession' });

// Simulate response for id 2 (because first timed out with id 1, second increments to 2)
// 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)
messageHandler({
data: {
target: INPAGE,
data: {
name: MULTICHAIN_SUBSTREAM_NAME,
data: {
id: 2,
id: MOCK_INITIAL_REQUEST_ID + 1,
result: mockSession,
},
},
Expand All @@ -340,6 +348,6 @@ describe('WindowPostMessageTransport', () => {
} as MessageEvent);

const result = await secondPromise;
expect(result).toEqual({ id: 2, result: mockSession });
expect(result).toEqual({ id: MOCK_INITIAL_REQUEST_ID + 1, result: mockSession });
});
});
4 changes: 2 additions & 2 deletions src/transports/windowPostMessageTransport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withTimeout } from '../helpers/utils';
import { getUniqueId, withTimeout } from '../helpers/utils';
import { TransportError, TransportTimeoutError } from '../types/errors';
import type { Transport, TransportResponse } from '../types/transport';
import { CONTENT_SCRIPT, DEFAULT_REQUEST_TIMEOUT, INPAGE, MULTICHAIN_SUBSTREAM_NAME } from './constants';
Expand All @@ -20,7 +20,7 @@ export function getWindowPostMessageTransport(params: { defaultTimeout?: number
const { defaultTimeout = DEFAULT_REQUEST_TIMEOUT } = params;
let messageListener: ((event: MessageEvent) => void) | null = null;
const pendingRequests: Map<number, (value: any) => void> = new Map();
let requestId = 1;
let requestId = getUniqueId();
/**
* Storing notification callbacks.
* If we detect a "notification" (a message without an id) coming from the extension, we'll call each callback in here.
Expand Down
Loading