Skip to content
This repository was archived by the owner on Feb 21, 2024. It is now read-only.

Commit 32e80b9

Browse files
authored
Merge pull request #341 from JoinColony/maintenance/metamask-v9-migration
Migrate metamask package to support Metamask v9
2 parents d684202 + 777bbb7 commit 32e80b9

File tree

9 files changed

+117
-50
lines changed

9 files changed

+117
-50
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "purser",
33
"private": true,
4-
"version": "3.0.0",
4+
"version": "3.1.0",
55
"description": "Interact with Ethereum wallets easily",
66
"scripts": {
77
"bootstrap": "lerna bootstrap",

packages/@purser/metamask/__tests__/helpers/detect.test.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,70 @@ describe('Metamask` Wallet Module', () => {
1212
await expect(detect()).rejects.toThrow();
1313
await expect(detect()).rejects.toThrow(new Error(messages.noExtension));
1414
});
15-
test('Checks if extension is unlocked', async () => {
15+
test('Checks if provider is connected to chain', async () => {
1616
/*
1717
* Mock the `isUnlocked()` ethereum method
1818
*/
19+
const isConnected = jest.fn(() => false);
20+
testGlobal.ethereum = {
21+
isConnected,
22+
};
23+
await expect(detect()).rejects.toThrow();
24+
await expect(detect()).rejects.toThrow(new Error(messages.noProvider));
25+
});
26+
test('Checks if the account is unlocked', async () => {
27+
/*
28+
* To reach this step, the extension already needs to be unlocked
29+
* (isUnlocked should return true)
30+
*
31+
* Mock the `isEnabled()` ethereum method
32+
*/
33+
const isConnected = jest.fn(() => true);
1934
const isUnlocked = jest.fn(async () => false);
2035
testGlobal.ethereum = {
21-
isUnlocked,
36+
isConnected,
37+
_metamask: {
38+
isUnlocked,
39+
},
2240
};
2341
await expect(detect()).rejects.toThrow();
2442
await expect(detect()).rejects.toThrow(new Error(messages.isLocked));
2543
});
26-
test('Checks if extension is enabled', async () => {
44+
test('Checks if we have permission to access the account', async () => {
2745
/*
2846
* To reach this step, the extension already needs to be unlocked
2947
* (isUnlocked should return true)
3048
*
3149
* Mock the `isEnabled()` ethereum method
3250
*/
51+
const request = jest.fn(async () => []);
52+
const isConnected = jest.fn(() => true);
3353
const isUnlocked = jest.fn(async () => true);
34-
const isEnabled = jest.fn(async () => false);
3554
testGlobal.ethereum = {
36-
isUnlocked,
37-
isEnabled,
55+
isConnected,
56+
request,
57+
_metamask: {
58+
isUnlocked,
59+
},
3860
};
3961
await expect(detect()).rejects.toThrow();
40-
await expect(detect()).rejects.toThrow(new Error(messages.notEnabled));
41-
});
42-
test('Checks if the proxy has the in-page provider set', async () => {
43-
testGlobal.ethereum = undefined;
44-
await expect(detect()).rejects.toThrow(new Error(messages.noExtension));
62+
await expect(detect()).rejects.toThrow(new Error(messages.notConnected));
4563
});
4664
test('Returns true if the extension is enabled', async () => {
4765
/*
4866
* Metamask is unlocked and enabled
4967
*/
68+
const request = jest.fn(async () => [
69+
{ parentCapability: 'eth_accounts' },
70+
]);
71+
const isConnected = jest.fn(() => true);
5072
const isUnlocked = jest.fn(async () => true);
51-
const isEnabled = jest.fn(async () => true);
5273
testGlobal.ethereum = {
53-
isUnlocked,
54-
isEnabled,
74+
isConnected,
75+
request,
76+
_metamask: {
77+
isUnlocked,
78+
},
5579
};
5680
await expect(detect()).resolves.not.toThrow();
5781
const wasDetected = await detect();

packages/@purser/metamask/__tests__/open.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,20 @@ const mockedWarning = mocked(warning);
1616
* Mocked values
1717
*/
1818
const mockedAddress = 'mocked-address';
19-
const mockedEnableMethod = jest.fn(() => [mockedAddress]);
19+
const mockedEnableMethod = jest.fn(async () => [mockedAddress]);
2020

2121
describe('Metamask` Wallet Module', () => {
2222
beforeEach(() => {
2323
testGlobal.ethereum = {
24-
enable: mockedEnableMethod,
25-
on: jest.fn(),
24+
request: mockedEnableMethod,
2625
};
2726
});
2827
afterEach(() => {
2928
mockedWarning.mockClear();
3029
mockedEnableMethod.mockClear();
3130
});
3231
describe('`open()` static method', () => {
33-
test('Start in EIP-1102 mode', async () => {
32+
test('Request accounts', async () => {
3433
await open();
3534
/*
3635
* Enabled the account

packages/@purser/metamask/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@purser/metamask",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"description": "A javascript library to interact with a Metamask based Ethereum wallet",
55
"license": "MIT",
66
"main": "lib/index.js",

packages/@purser/metamask/src/helpers.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { helpers as messages } from './messages';
22

3-
import { AccountsChangedCallback } from './types';
3+
import { AccountsChangedCallback, ObservableEvents } from './types';
44

55
// eslint-disable-next-line @typescript-eslint/no-explicit-any
66
const anyGlobal: any = global;
@@ -20,39 +20,37 @@ export const detect = async (): Promise<boolean> => {
2020
* Modern Metamask Version
2121
*/
2222
if (anyGlobal.ethereum) {
23+
const { ethereum } = anyGlobal;
2324
/*
24-
* @NOTE This is a temporary failsafe check, since Metmask is running an
25-
* intermediate version which, while it contains most of the `ethereum`
26-
* global object, it doesn't contain this helper method
25+
* Check that the provider is connected to the chain
26+
*/
27+
if (!ethereum.isConnected()) {
28+
throw new Error(messages.noProvider);
29+
}
30+
/*
31+
* Check if the account is unlocked
2732
*
28-
* @TODO Remove legacy metmask object availability check
29-
* After an adequate amount of time has passed
33+
* @NOTE we just assume the required methods exist on the metamask provider
34+
* otherwise we'll get right back to "support legacy metamask hell"
3035
*/
31-
if (
32-
anyGlobal.ethereum.isUnlocked &&
33-
!(await anyGlobal.ethereum.isUnlocked())
34-
) {
36+
// eslint-disable-next-line no-underscore-dangle
37+
if (!(await ethereum._metamask.isUnlocked())) {
3538
throw new Error(messages.isLocked);
3639
}
3740
/*
38-
* @NOTE This is a temporary failsafe check, since Metmask is running an
39-
* intermediate version which, while it contains most of the `ethereum`
40-
* global object, it doesn't contain this helper method
41-
*
42-
* @TODO Remove legacy metmask object availability check
43-
* After an adequate amount of time has passed
41+
* If we don't have the `eth_accounts` permissions it means that we don't have
42+
* account access
4443
*/
44+
const permissions = await ethereum.request({
45+
method: 'wallet_getPermissions',
46+
});
4547
if (
46-
anyGlobal.ethereum.isEnabled &&
47-
!(await anyGlobal.ethereum.isEnabled())
48+
!permissions.length ||
49+
!permissions[0] ||
50+
permissions[0].parentCapability !== 'eth_accounts'
4851
) {
49-
throw new Error(messages.notEnabled);
52+
throw new Error(messages.notConnected);
5053
}
51-
/*
52-
* @NOTE If the `isUnlocked` and the `isEnabled` methods are not available
53-
* it means we are running the pre-release version of Metamask, just prior
54-
* to the EIP-1102 update, so we just ignore those checks
55-
*/
5654
return true;
5755
}
5856
throw new Error(messages.noExtension);
@@ -103,7 +101,8 @@ export const methodCaller = async <T>(
103101
*/
104102
export const setStateEventObserver = (
105103
callback: AccountsChangedCallback,
104+
observableEvent: ObservableEvents = 'accountsChanged',
106105
): void => {
107106
const { ethereum } = anyGlobal;
108-
ethereum.on('accountsChanged', callback);
107+
ethereum.on(observableEvent, callback);
109108
};

packages/@purser/metamask/src/index.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@ export const messages = staticMethods;
2626
*/
2727
export const open = async (): Promise<MetaMaskWallet> => {
2828
let addressAfterEnable: string;
29+
detectHelper();
2930
try {
3031
/*
3132
* See: https://eips.ethereum.org/EIPS/eip-1102
3233
*/
3334
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3435
const anyGlobal: any = global;
3536
if (anyGlobal.ethereum) {
36-
[addressAfterEnable] = await anyGlobal.ethereum.enable();
37+
const { ethereum } = anyGlobal;
38+
[addressAfterEnable] = await ethereum.request({
39+
method: 'eth_requestAccounts',
40+
});
3741
}
3842
return methodCaller(() => {
3943
return new MetaMaskWallet({
@@ -75,7 +79,6 @@ export const detect = async (): Promise<boolean> => detectHelper();
7579
* @method accountChangeHook
7680
*
7781
* @param {Function} callback Function to add the state events update array
78-
* It receives the state object as an only argument
7982
*
8083
* @return {Promise<void>} Does not return noting
8184
*/
@@ -100,3 +103,37 @@ export const accountChangeHook = async (
100103
throw new Error(messages.cannotAddHook);
101104
}
102105
};
106+
107+
/**
108+
* Hook into Metamask's state events observers array to be able to act on account
109+
* changes from the UI
110+
*
111+
* It's a wrapper around the `setStateEventObserver()` helper method
112+
*
113+
* @method chainChangeHook
114+
*
115+
* @param {Function} callback Function to add the state events update array
116+
*
117+
* @return {Promise<void>} Does not return noting
118+
*/
119+
export const chainChangeHook = async (
120+
callback: AccountsChangedCallback,
121+
): Promise<void> => {
122+
/*
123+
* If detect fails, it will throw, with a message explaining the problem
124+
* (Most likely Metamask will be locked, so we won't be able to get to
125+
* the state observer via the in-page provider)
126+
*/
127+
detectHelper();
128+
try {
129+
return setStateEventObserver(callback, 'chainChanged');
130+
} catch (error) {
131+
/*
132+
* If this throws/catches here it means something very weird is going on.
133+
* `detect()` should catch anything that're directly related to Metamask's functionality,
134+
* but if that passes and we have to catch it here, it means some underlying APIs
135+
* might have changed, and this will be very hard to debug
136+
*/
137+
throw new Error(messages.cannotAddHook);
138+
}
139+
};

packages/@purser/metamask/src/messages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ export const helpers = {
3232
noExtension:
3333
"Could not detect the Metamask extension. Ensure that it's enabled",
3434
isLocked: "Metamask's instance is locked. Please unlock it from the UI",
35-
notEnabled:
36-
'The Metmask extension instance has not been enabled on this page. Please use the `open()` method to do so.',
35+
notConnected:
36+
'The Metmask account is not connected to the current domain. Please manually connected from the extension itself',
37+
noProvider: 'The Metmask provider is not connected to the chain.',
3738
/*
3839
* Legacy Metamask Version
3940
*

packages/@purser/metamask/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ export interface SignMessageObject {
99
message: string;
1010
messageData: string | Uint8Array;
1111
}
12+
13+
export type ObservableEvents =
14+
| 'accountsChanged'
15+
| 'chainChanged'
16+
| 'connect'
17+
| 'disconnect'
18+
| 'message';

packages/@purser/signer-ethers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@purser/signer-ethers",
3-
"version": "1.0.1-alpha.0",
3+
"version": "1.0.1",
44
"description": "A signer to use purser with ethers.js",
55
"license": "MIT",
66
"main": "lib/index.js",

0 commit comments

Comments
 (0)