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

Revert "Remove deprecated properties, networkChanged event, and offli… #312

Merged
merged 1 commit into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ const baseConfig = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 61.45,
functions: 63.91,
lines: 63.59,
statements: 63.65,
branches: 65.43,
functions: 65.65,
lines: 66.74,
statements: 66.81,
},
},

Expand Down
55 changes: 39 additions & 16 deletions src/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,28 +230,33 @@ export abstract class BaseProvider extends SafeEventEmitter {
* Sets initial state if provided and marks this provider as initialized.
* Throws if called more than once.
*
* Permits the `networkVersion` field in the parameter object for
* compatibility with child classes that use this value.
*
* @param initialState - The provider's initial state.
* @param initialState.accounts - The user's accounts.
* @param initialState.chainId - The chain ID.
* @param initialState.isUnlocked - Whether the user has unlocked MetaMask.
* @param initialState.networkVersion - The network version.
* @fires BaseProvider#_initialized - If `initialState` is defined.
* @fires BaseProvider#connect - If `initialState` is defined.
*/
protected _initializeState(initialState?: {
accounts: string[];
chainId: string;
isUnlocked: boolean;
networkVersion?: string;
}) {
if (this._state.initialized) {
throw new Error('Provider already initialized.');
}

if (initialState) {
const { accounts, chainId, isUnlocked } = initialState;
const { accounts, chainId, isUnlocked, networkVersion } = initialState;

// EIP-1193 connect
this._handleConnect(chainId);
this._handleChainChanged({ chainId });
this._handleChainChanged({ chainId, networkVersion });
this._handleUnlockStateChanged({ accounts, isUnlocked });
this._handleAccountsChanged(accounts);
}
Expand Down Expand Up @@ -324,23 +329,36 @@ export abstract class BaseProvider extends SafeEventEmitter {
* Error codes per the CloseEvent status codes as required by EIP-1193:
* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes.
*
* @param isRecoverable - Whether the disconnection is recoverable.
* @param errorMessage - A custom error message.
* @fires BaseProvider#disconnect
* @fires BaseProvider#disconnect - If the disconnection is not recoverable.
*/
protected _handleDisconnect(errorMessage?: string) {
if (this._state.isConnected || !this._state.isPermanentlyDisconnected) {
protected _handleDisconnect(isRecoverable: boolean, errorMessage?: string) {
if (
this._state.isConnected ||
(!this._state.isPermanentlyDisconnected && !isRecoverable)
) {
this._state.isConnected = false;

const error = new JsonRpcError(
1011, // Internal error
errorMessage ?? messages.errors.permanentlyDisconnected(),
);
this._log.error(error);
this.#chainId = null;
this._state.accounts = null;
this.#selectedAddress = null;
this._state.isUnlocked = false;
this._state.isPermanentlyDisconnected = true;
let error;
if (isRecoverable) {
error = new JsonRpcError(
1013, // Try again later
errorMessage ?? messages.errors.disconnected(),
);
this._log.debug(error);
} else {
error = new JsonRpcError(
1011, // Internal error
errorMessage ?? messages.errors.permanentlyDisconnected(),
);
this._log.error(error);
this.#chainId = null;
this._state.accounts = null;
this.#selectedAddress = null;
this._state.isUnlocked = false;
this._state.isPermanentlyDisconnected = true;
}

this.emit('disconnect', error);
}
Expand All @@ -351,13 +369,18 @@ export abstract class BaseProvider extends SafeEventEmitter {
* and sets relevant public state. Does nothing if the given `chainId` is
* equivalent to the existing value.
*
* Permits the `networkVersion` field in the parameter object for
* compatibility with child classes that use this value.
*
* @fires BaseProvider#chainChanged
* @param networkInfo - An object with network info.
* @param networkInfo.chainId - The latest chain ID.
*/
protected _handleChainChanged({
chainId,
}: { chainId?: string | undefined } | undefined = {}) {
}:
| { chainId?: string | undefined; networkVersion?: string | undefined }
| undefined = {}) {
if (!isValidChainId(chainId)) {
this._log.error(messages.errors.invalidNetworkParams(), { chainId });
return;
Expand Down
155 changes: 143 additions & 12 deletions src/MetaMaskInpageProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ type InitializedProviderDetails = {
* can be used to inspect message sent by the provider.
*/
async function getInitializedProvider({
initialState: { accounts = [], chainId = '0x0', isUnlocked = true } = {},
initialState: {
accounts = [],
chainId = '0x0',
isUnlocked = true,
networkVersion = '0',
} = {},
onMethodCalled = [],
}: {
initialState?: Partial<
Expand Down Expand Up @@ -73,6 +78,7 @@ async function getInitializedProvider({
accounts,
chainId,
isUnlocked,
networkVersion,
},
}),
);
Expand Down Expand Up @@ -707,6 +713,13 @@ describe('MetaMaskInpageProvider: RPC', () => {
expect.any(Function),
);
});

it('net_version', () => {
const result = provider.send({ method: 'net_version' });
expect(result).toMatchObject({
result: null,
});
});
});

it('throws on unsupported sync method', () => {
Expand Down Expand Up @@ -735,9 +748,84 @@ describe('MetaMaskInpageProvider: RPC', () => {
connectionStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1' },
params: { chainId: '0x1', networkVersion: '1' },
});
});
});

it('calls networkChanged when receiving a new networkVersion', async () => {
const { provider, connectionStream } = await getInitializedProvider();

await new Promise((resolve) => {
provider.once('networkChanged', (newNetworkId) => {
expect(newNetworkId).toBe('1');
resolve(undefined);
});

connectionStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
});
});
});

it('handles chain changes with intermittent disconnection', async () => {
const { provider, connectionStream } = await getInitializedProvider();

// We check this mostly for the readability of this test.
expect(provider.isConnected()).toBe(true);
expect(provider.chainId).toBe('0x0');
expect(provider.networkVersion).toBe('0');

const emitSpy = jest.spyOn(provider, 'emit');

await new Promise<void>((resolve) => {
provider.once('disconnect', (error) => {
expect((error as any).code).toBe(1013);
resolve();
});

connectionStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
// A "loading" networkVersion indicates the network is changing.
// Although the chainId is different, chainChanged should not be
// emitted in this case.
params: { chainId: '0x1', networkVersion: 'loading' },
});
});

// Only once, for "disconnect".
expect(emitSpy).toHaveBeenCalledTimes(1);
emitSpy.mockClear(); // Clear the mock to avoid keeping a count.

expect(provider.isConnected()).toBe(false);
// These should be unchanged.
expect(provider.chainId).toBe('0x0');
expect(provider.networkVersion).toBe('0');

await new Promise<void>((resolve) => {
provider.once('chainChanged', (newChainId) => {
expect(newChainId).toBe('0x1');
resolve();
});

connectionStream.notify(MetaMaskInpageProviderStreamName, {
jsonrpc: '2.0',
method: 'metamask_chainChanged',
params: { chainId: '0x1', networkVersion: '1' },
});
});

expect(emitSpy).toHaveBeenCalledTimes(3);
expect(emitSpy).toHaveBeenNthCalledWith(1, 'connect', { chainId: '0x1' });
expect(emitSpy).toHaveBeenCalledWith('chainChanged', '0x1');
expect(emitSpy).toHaveBeenCalledWith('networkChanged', '1');

expect(provider.isConnected()).toBe(true);
expect(provider.chainId).toBe('0x1');
expect(provider.networkVersion).toBe('1');
});
});

Expand Down Expand Up @@ -940,6 +1028,7 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
accounts: ['0xabc'],
chainId: '0x0',
isUnlocked: true,
networkVersion: '0',
};
});

Expand All @@ -948,6 +1037,9 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {

await new Promise<void>((resolve) => setTimeout(() => resolve(), 1));
expect(requestMock).toHaveBeenCalledTimes(1);
expect(inpageProvider.chainId).toBe('0x0');
expect(inpageProvider.networkVersion).toBe('0');
expect(inpageProvider.selectedAddress).toBe('0xabc');
expect(inpageProvider.isConnected()).toBe(true);
});
});
Expand Down Expand Up @@ -992,24 +1084,52 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
).provider;
});

it('should throw an error when accessing chainId', () => {
expect(() => provider.chainId).toThrow(
`'ethereum.chainId' has been removed`,
it('should warn the first time chainId is accessed', async () => {
const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn');

expect(provider.chainId).toBe('0x5');
expect(consoleWarnSpy).toHaveBeenCalledWith(
messages.warnings.chainIdDeprecation,
);
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('should not allow chainId to be modified', () => {
expect(() => (provider.chainId = '0x539')).toThrow(
'Cannot set property chainId',
);
expect(provider.chainId).toBe('0x5');
});
});

describe('networkVersion', () => {
let provider: any | MetaMaskInpageProvider;

beforeEach(async () => {
provider = (await getInitializedProvider()).provider;
provider = (
await getInitializedProvider({
initialState: {
networkVersion: '5',
},
})
).provider;
});

it('should throw an error when accessing networkVersion', () => {
expect(() => provider.networkVersion).toThrow(
`'ethereum.networkVersion' has been removed`,
it('should warn the first time networkVersion is accessed', async () => {
const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn');

expect(provider.networkVersion).toBe('5');
expect(consoleWarnSpy).toHaveBeenCalledWith(
messages.warnings.networkVersionDeprecation,
);
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('should not allow networkVersion to be modified', () => {
expect(() => (provider.networkVersion = '1337')).toThrow(
'Cannot set property networkVersion',
);
expect(provider.networkVersion).toBe('5');
});
});

Expand All @@ -1026,10 +1146,21 @@ describe('MetaMaskInpageProvider: Miscellanea', () => {
).provider;
});

it('should throw an error when accessing selectedAddress', () => {
expect(() => provider.selectedAddress).toThrow(
`'ethereum.selectedAddress' has been removed`,
it('should warn the first time selectedAddress is accessed', async () => {
const consoleWarnSpy = jest.spyOn(globalThis.console, 'warn');

expect(provider.selectedAddress).toBe('0xdeadbeef');
expect(consoleWarnSpy).toHaveBeenCalledWith(
messages.warnings.selectedAddressDeprecation,
);
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('should not allow selectedAddress to be modified', () => {
expect(() => (provider.selectedAddress = '0x12345678')).toThrow(
'Cannot set property selectedAddress',
);
expect(provider.selectedAddress).toBe('0xdeadbeef');
});
});
});
Loading
Loading