Skip to content

Commit

Permalink
fix: select proper MetaMask provider when conflicts (#34)
Browse files Browse the repository at this point in the history
* feat: update eth-testing version

* fix: filter providers in ethereum object of window

* fix: use providers instead of providerMap

* feat: update test titles

* feat: update comment
  • Loading branch information
VGLoic authored Dec 4, 2022
1 parent b9291de commit 01d099e
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 33 deletions.
53 changes: 40 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-testing-library": "^3.10.1",
"eth-testing": "^1.1.0-alpha.15",
"eth-testing": "^1.9.1",
"husky": "^4.3.7",
"jest": "^26.6.3",
"lint-staged": "^10.5.3",
Expand Down
85 changes: 85 additions & 0 deletions src/__tests__/ethereum-conflicts.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { renderHook } from "@testing-library/react-hooks";
import { generateTestingUtils } from "eth-testing";

import { useMetaMask, MetaMaskProvider } from "../";

describe("`window.ethereum` conflict tests", () => {
describe("when the `ethereum` object has a `providerMap` field", () => {
test("when the `providers` does not have a `MetaMask` provider, it should synchronise in `unavailable` status", async () => {
let originalEth = (window as any).ethereum;
const testingUtils = generateTestingUtils();
const coinbaseProvider = testingUtils.getProvider();
const ethereum = {
providers: [coinbaseProvider],
};
(window as any).ethereum = ethereum;

const { result } = renderHook(useMetaMask, { wrapper: MetaMaskProvider });

expect(result.current.status).toEqual("unavailable");

(window as any).ethereum = originalEth;
});

test("when the `providers` does have a valid MetaMask provider, it should synchronise in `notConnected` status", async () => {
let originalEth = (window as any).ethereum;
const testingUtils = generateTestingUtils();
const coinbaseProvider = testingUtils.getProvider();
const metaMaskTestingUtils = generateTestingUtils({
providerType: "MetaMask",
});
const metaMaskProvider = metaMaskTestingUtils.getProvider();
const ethereum = {
providers: [coinbaseProvider, metaMaskProvider],
};
(window as any).ethereum = ethereum;

metaMaskTestingUtils.mockNotConnectedWallet();

const { result, waitForNextUpdate } = renderHook(useMetaMask, {
wrapper: MetaMaskProvider,
});

expect(result.current.status).toEqual("initializing");

await waitForNextUpdate();

expect(result.current.status).toEqual("notConnected");

(window as any).ethereum = originalEth;
});
});

test("when the `MetaMask` provider is corrupted or removed in the meantime, it should throw", async () => {
let originalEth = (window as any).ethereum;
const testingUtils = generateTestingUtils();
const coinbaseProvider = testingUtils.getProvider();
const metaMaskTestingUtils = generateTestingUtils({
providerType: "MetaMask",
});
const metaMaskProvider = metaMaskTestingUtils.getProvider();
const providers = [coinbaseProvider, metaMaskProvider];
const ethereum = { providers };
(window as any).ethereum = ethereum;

metaMaskTestingUtils.mockNotConnectedWallet();

const { result, waitForNextUpdate } = renderHook(useMetaMask, {
wrapper: MetaMaskProvider,
});

expect(result.current.status).toEqual("initializing");

await waitForNextUpdate();

expect(result.current.status).toEqual("notConnected");

providers.pop();

expect(() => result.current.connect()).toThrowError(
"MetaMask provider must be present in order to use this method"
);

(window as any).ethereum = originalEth;
});
});
15 changes: 12 additions & 3 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { act, renderHook } from "@testing-library/react-hooks";
import { setupEthTesting } from "eth-testing";
import { generateTestingUtils } from "eth-testing";

import { useMetaMask, MetaMaskProvider } from "../";

Expand All @@ -22,6 +22,15 @@ describe("MetaMask provider", () => {

describe("when MetaMask is not available", () => {
test("when there is no `ethereum` object in the window, it should synchronise into `unavailable` status", async () => {
let originalEth = (window as any).ethereum;
const testingUtils = generateTestingUtils();
const unknownProvider = testingUtils.getProvider();
(window as any).ethereum = unknownProvider;
const { result } = renderHook(useMetaMask, { wrapper: MetaMaskProvider });
expect(result.current.status).toEqual("unavailable");
(window as any).ethereum = originalEth;
});
test("when there is an `ethereum` object in the window but it is not a MetaMask type, it should synchronise into `unavailable` status", async () => {
const { result } = renderHook(useMetaMask, { wrapper: MetaMaskProvider });

expect(result.current.status).toEqual("unavailable");
Expand Down Expand Up @@ -65,13 +74,13 @@ describe("MetaMask provider", () => {

describe("when MetaMask is available", () => {
let originalEth: any;
const { provider: ethereum, testingUtils } = setupEthTesting({
const testingUtils = generateTestingUtils({
providerType: "MetaMask",
});

beforeAll(() => {
originalEth = (window as any).ethereum;
(window as any).ethereum = ethereum;
(window as any).ethereum = testingUtils.getProvider();
});

afterAll(() => {
Expand Down
12 changes: 7 additions & 5 deletions src/__tests__/use-metamask.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderHook } from "@testing-library/react-hooks";
import { setupEthTesting } from "eth-testing";
import { generateTestingUtils } from "eth-testing";
import { MetaMaskProvider } from "..";
import { useConnectedMetaMask, useMetaMask } from "../use-metamask";

Expand Down Expand Up @@ -37,12 +37,12 @@ describe("useMetaMask", () => {
});

test("calling useConnectedMetaMask when the status is `connected should return the expected values", async () => {
const { provider: ethereum, testingUtils } = setupEthTesting({
const testingUtils = generateTestingUtils({
providerType: "MetaMask",
});

let originalEth = (window as any).ethereum;
(window as any).ethereum = ethereum;
(window as any).ethereum = testingUtils.getProvider();

testingUtils.mockConnectedWallet([
"0x19F7Fa0a30d5829acBD9B35bA2253a759a37EfC6",
Expand All @@ -63,10 +63,12 @@ describe("useMetaMask", () => {

await waitForNextUpdate();

expect(result.current.account).toEqual(
const current = result.current as { account: string; chainId: string };

expect(current.account).toEqual(
"0x19F7Fa0a30d5829acBD9B35bA2253a759a37EfC6"
);
expect(result.current.chainId).toEqual("0x1");
expect(current.chainId).toEqual("0x1");

(window as any).ethereum = originalEth;
});
Expand Down
44 changes: 33 additions & 11 deletions src/metamask-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,34 @@ const ERROR_CODE_REQUEST_PENDING = -32002;
type WindowInstanceWithEthereum = Window &
typeof globalThis & { ethereum?: any };

async function synchronize(dispatch: (action: Action) => void) {
function getMetaMaskProvider() {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const isMetaMaskAvailable = Boolean(ethereum) && ethereum.isMetaMask;
if (!isMetaMaskAvailable) {
if (!ethereum) return null;
// The `providers` field is populated when CoinBase Wallet extension is also installed
// The expected object is an array of providers, the MetaMask provider is inside
// See https://docs.cloud.coinbase.com/wallet-sdk/docs/injected-provider-guidance for more information
if (Array.isArray(ethereum.providers)) {
const metaMaskProvider = ethereum.providers.find((p: any) => p.isMetaMask);
if (!metaMaskProvider) return null;
return metaMaskProvider;
}
if (!ethereum.isMetaMask) return null;
return ethereum;
}

function getSafeMetaMaskProvider() {
const ethereum = getMetaMaskProvider();
if (!ethereum) {
throw new Error(
"MetaMask provider must be present in order to use this method"
);
}
return ethereum;
}

async function synchronize(dispatch: (action: Action) => void) {
const ethereum = getMetaMaskProvider();
if (!ethereum) {
dispatch({ type: "metaMaskUnavailable" });
return;
}
Expand All @@ -45,7 +69,7 @@ async function synchronize(dispatch: (action: Action) => void) {
}

function subsribeToAccountsChanged(dispatch: (action: Action) => void) {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const ethereum = getSafeMetaMaskProvider();
const onAccountsChanged = (accounts: string[]) =>
dispatch({ type: "metaMaskAccountsChanged", payload: accounts });
ethereum.on("accountsChanged", onAccountsChanged);
Expand All @@ -55,7 +79,7 @@ function subsribeToAccountsChanged(dispatch: (action: Action) => void) {
}

function subscribeToChainChanged(dispatch: (action: Action) => void) {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const ethereum = getSafeMetaMaskProvider();
const onChainChanged = (chainId: string) =>
dispatch({ type: "metaMaskChainChanged", payload: chainId });
ethereum.on("chainChanged", onChainChanged);
Expand All @@ -67,7 +91,7 @@ function subscribeToChainChanged(dispatch: (action: Action) => void) {
function requestAccounts(
dispatch: (action: Action) => void
): Promise<string[]> {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const ethereum = getSafeMetaMaskProvider();

dispatch({ type: "metaMaskConnecting" });

Expand Down Expand Up @@ -119,7 +143,7 @@ function requestAccounts(
}

async function addEthereumChain(parameters: AddEthereumChainParameter) {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const ethereum = getSafeMetaMaskProvider();
try {
await ethereum.request({
method: "wallet_addEthereumChain",
Expand All @@ -134,7 +158,7 @@ async function addEthereumChain(parameters: AddEthereumChainParameter) {
}

async function switchEthereumChain(chainId: string) {
const ethereum = (window as WindowInstanceWithEthereum).ethereum;
const ethereum = getSafeMetaMaskProvider();
try {
await ethereum.request({
method: "wallet_switchEthereumChain",
Expand Down Expand Up @@ -223,9 +247,7 @@ export function MetaMaskProvider(props: any) {
connect,
addChain,
switchChain,
ethereum: isAvailable
? (window as WindowInstanceWithEthereum).ethereum
: null,
ethereum: isAvailable ? getSafeMetaMaskProvider() : null,
}),
[connect, addChain, switchChain, state, isAvailable]
);
Expand Down

0 comments on commit 01d099e

Please sign in to comment.