Skip to content

Commit 2252b59

Browse files
authored
feat: MetaMask Connect (#433)
* docs: plan connect evm integration * docs: add connect evm implementation plan * chore: ignore local worktrees * chore: fix connect evm plan baseline lint * feat: add connect evm dependency * chore: require node 20 * chore: update node version file * feat: add connect evm client helper * fix: harden connect evm helper * fix: allow connect evm init retry * feat: add connect evm button * fix: tighten connect evm button text * feat: wire connect evm button * feat: support eip1193 provider initialization * fix: clean up active provider listeners * fix: keep walletconnect disconnect state cleared * fix: clear active provider teardown state * fix: resolve process browser shim * fix: tighten provider disconnect lifecycle * chore: remove plan docs * fix: rename connect evm button label * fix: drop unused connect evm provider tracking * fix: tidy connect evm icon and install event dispatch * ci: drop node 18 from build-lint-test matrix `@metamask/connect-evm` raises the project's node floor to >=20.19.0 (reflected in package.json engines and .nvmrc), so testing on node 18.x no longer matches a supported runtime. Run the matrix on 20.x only. * chore: allowlist install scripts for connect evm deps `yarn allow-scripts` failed in CI because the @metamask/connect-evm dependency tree introduced a package with an install script (protobufjs, via connect-multichain > mobile-wallet-protocol-core > centrifuge) and re-resolved keccak/secp256k1 under ethereum-cryptography, which no longer matched the existing allowlist paths. Ran `yarn allow-scripts auto`; the new entries default to false (scripts disabled), consistent with every other entry in the config.
1 parent 5157bd3 commit 2252b59

13 files changed

Lines changed: 575 additions & 97 deletions

File tree

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
{
2020
files: ['src/**/*.js'],
2121
parserOptions: {
22+
ecmaVersion: 2020,
2223
sourceType: 'module',
2324
},
2425
},

.github/workflows/build-lint-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
node-version: [18.x, 20.x]
14+
node-version: [20.x]
1515
steps:
1616
- uses: actions/checkout@v2
1717
- name: Use Node.js ${{ matrix.node-version }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_modules
44
dist
55
.eslintcache
66
.DS_Store
7+
.worktrees

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v18
1+
v20.19.1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ It can be used by navigating to `/request.html?method=${METHOD}&params=${PARAMS}
2222

2323
### Setup
2424

25-
- Install [Node.js](https://nodejs.org) version 16
25+
- Install [Node.js](https://nodejs.org) version 20.19.0 or later
2626
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
2727
- Install [Yarn v1](https://yarnpkg.com/en/docs/install)
2828
- Run `yarn setup` to install dependencies and run any required post-install scripts

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "9.9.0",
44
"description": "A simple dapp used in MetaMask e2e tests.",
55
"engines": {
6-
"node": ">= 18.0.0"
6+
"node": ">=20.19.0"
77
},
88
"scripts": {
99
"setup": "yarn install && yarn allow-scripts",
@@ -42,6 +42,7 @@
4242
"@lavamoat/allow-scripts": "^2.5.1",
4343
"@lavamoat/preinstall-always-fail": "^2.0.0",
4444
"@metamask/auto-changelog": "^2.5.0",
45+
"@metamask/connect-evm": "^1.4.0",
4546
"@metamask/eslint-config": "^6.0.0",
4647
"@metamask/eslint-config-nodejs": "^6.0.0",
4748
"@metamask/eth-sig-util": "^7.0.1",
@@ -82,7 +83,10 @@
8283
"@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false,
8384
"@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false,
8485
"@metamask/sdk>eciesjs>secp256k1": false,
85-
"@web3modal/ethers5>@coinbase/wallet-sdk>@solana/web3.js>rpc-websockets>utf-8-validate": false
86+
"@web3modal/ethers5>@coinbase/wallet-sdk>@solana/web3.js>rpc-websockets>utf-8-validate": false,
87+
"@metamask/connect-evm>@metamask/connect-multichain>@metamask/mobile-wallet-protocol-core>centrifuge>protobufjs": false,
88+
"ethereumjs-util>ethereum-cryptography>keccak": false,
89+
"ethereumjs-util>ethereum-cryptography>secp256k1": false
8690
}
8791
},
8892
"packageManager": "yarn@1.22.22"

src/components/connections/connections.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export function connectionsComponent(parentContainer) {
3131
>
3232
SDK Connect
3333
</button>
34+
<button
35+
class="btn btn-primary btn-lg btn-block mb-3"
36+
id="connectEvm"
37+
>MetaMask Connect</button>
3438
<hr />
3539
<button
3640
class="btn btn-primary btn-lg btn-block mb-3"
@@ -51,6 +55,7 @@ export function connectionsComponent(parentContainer) {
5155
const onboardButton = document.getElementById('connectButton');
5256
const walletConnectBtn = document.getElementById('walletConnect');
5357
const sdkConnectBtn = document.getElementById('sdkConnect');
58+
const connectEvmBtn = document.getElementById('connectEvm');
5459
*/
5560
const getAccounts = document.getElementById('getAccounts');
5661
const getAccountsResult = document.getElementById('getAccountsResult');

src/connect-evm.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { createEVMClient } from '@metamask/connect-evm';
2+
import dappMetadata from './dapp-metadata';
3+
import globalContext, {
4+
handleNewAccounts,
5+
handleNewProviderDetail,
6+
removeProviderDetail,
7+
setActiveProviderDetail,
8+
updateFormElements,
9+
} from '.';
10+
11+
export const CONNECT_EVM_PROVIDER_UUID = 'connect-evm';
12+
13+
export const CONNECT_EVM_SUPPORTED_NETWORKS = {
14+
'0x1': 'https://ethereum.publicnode.com',
15+
'0xaa36a7': 'https://ethereum-sepolia.publicnode.com',
16+
'0xa': 'https://optimism.publicnode.com',
17+
'0x89': 'https://polygon-bor-rpc.publicnode.com',
18+
'0x2105': 'https://base.publicnode.com',
19+
'0xa4b1': 'https://arbitrum-one.publicnode.com',
20+
'0xa86a': 'https://avalanche-c-chain-rpc.publicnode.com',
21+
'0x38': 'https://bsc-dataseed.binance.org',
22+
'0x539': 'http://127.0.0.1:8545',
23+
'0x53a': 'http://127.0.0.1:8546',
24+
};
25+
26+
export const CONNECT_EVM_CHAIN_IDS = Object.keys(
27+
CONNECT_EVM_SUPPORTED_NETWORKS,
28+
);
29+
30+
let connectEvmClientPromise;
31+
32+
const noop = () => undefined;
33+
34+
async function getConnectEvmClient() {
35+
if (!connectEvmClientPromise) {
36+
connectEvmClientPromise = createEVMClient({
37+
dapp: dappMetadata,
38+
api: {
39+
supportedNetworks: CONNECT_EVM_SUPPORTED_NETWORKS,
40+
},
41+
});
42+
}
43+
44+
try {
45+
return await connectEvmClientPromise;
46+
} catch (err) {
47+
connectEvmClientPromise = undefined;
48+
throw err;
49+
}
50+
}
51+
52+
function getConnectEvmProviderDetail(provider, name) {
53+
return {
54+
info: {
55+
uuid: CONNECT_EVM_PROVIDER_UUID,
56+
name,
57+
icon: './metamask-fox.svg',
58+
rdns: 'io.metamask',
59+
},
60+
provider,
61+
};
62+
}
63+
64+
function setConnectedButtonState(button) {
65+
button.innerText = 'MetaMask Connect - Disconnect';
66+
button.classList.remove('btn-primary');
67+
button.classList.add('btn-danger');
68+
}
69+
70+
function setDisconnectedButtonState(button) {
71+
button.innerText = 'MetaMask Connect';
72+
button.classList.add('btn-primary');
73+
button.classList.remove('btn-danger');
74+
}
75+
76+
export async function handleConnectEvm(
77+
name,
78+
button,
79+
isConnected,
80+
updateConnectionState = noop,
81+
) {
82+
button.disabled = true;
83+
84+
try {
85+
const client = await getConnectEvmClient();
86+
87+
if (isConnected) {
88+
await client.disconnect();
89+
updateConnectionState(false);
90+
const activeProviderRemoved = removeProviderDetail(name);
91+
setDisconnectedButtonState(button);
92+
if (activeProviderRemoved) {
93+
updateFormElements();
94+
}
95+
return;
96+
}
97+
98+
const { accounts } = await client.connect({
99+
chainIds: CONNECT_EVM_CHAIN_IDS,
100+
});
101+
const provider = client.getProvider();
102+
103+
const providerDetail = getConnectEvmProviderDetail(provider, name);
104+
await setActiveProviderDetail(providerDetail);
105+
handleNewProviderDetail(providerDetail);
106+
updateConnectionState(true);
107+
setConnectedButtonState(button);
108+
updateFormElements();
109+
handleNewAccounts(accounts);
110+
globalContext.connected = true;
111+
} catch (err) {
112+
console.error('Error connecting with MetaMask Connect EVM', err);
113+
if (isConnected) {
114+
setConnectedButtonState(button);
115+
} else {
116+
setDisconnectedButtonState(button);
117+
}
118+
} finally {
119+
button.disabled = false;
120+
}
121+
}

src/connections.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MetaMaskSDK } from '@metamask/sdk';
2+
import dappMetadata from './dapp-metadata';
23
import globalContext, {
34
handleNewAccounts,
45
handleNewProviderDetail,
@@ -9,12 +10,6 @@ import globalContext, {
910
updateWalletConnectState,
1011
} from '.';
1112

12-
const dappMetadata = {
13-
name: 'E2e Test Dapp',
14-
description: 'This is the E2e Test Dapp',
15-
url: 'https://metamask.github.io/test-dapp/',
16-
};
17-
1813
const sdk = new MetaMaskSDK({ dappMetadata });
1914

2015
export const initializeWeb3Modal = () => {
@@ -52,14 +47,15 @@ function _setProviderDetail(provider, name, uuid) {
5247

5348
export async function handleSdkConnect(name, button, isConnected) {
5449
if (isConnected) {
55-
handleNewAccounts([]);
56-
updateFormElements();
5750
updateSdkConnectionState(false);
58-
removeProviderDetail(name);
51+
const activeProviderRemoved = removeProviderDetail(name);
5952
await sdk.terminate();
6053
button.innerText = 'Sdk Connect';
6154
button.classList.add('btn-primary');
6255
button.classList.remove('btn-danger');
56+
if (activeProviderRemoved) {
57+
updateFormElements();
58+
}
6359
} else {
6460
await sdk.connect();
6561
const provider = sdk.getProvider();
@@ -87,14 +83,14 @@ export async function handleSdkConnect(name, button, isConnected) {
8783

8884
export async function handleWalletConnect(name, button, isConnected) {
8985
if (isConnected) {
90-
handleNewAccounts([]);
91-
updateFormElements();
9286
updateWalletConnectState(false);
93-
removeProviderDetail(name);
87+
const activeProviderRemoved = removeProviderDetail(name);
9488
button.innerText = 'Wallet Connect';
9589
button.classList.add('btn-primary');
9690
button.classList.remove('btn-danger');
97-
globalContext.connected = false;
91+
if (activeProviderRemoved) {
92+
updateFormElements();
93+
}
9894
} else {
9995
const { provider } = walletConnect.getWalletProvider();
10096
const uuid = provider.signer.uri;
@@ -116,6 +112,6 @@ export async function handleWalletConnect(name, button, isConnected) {
116112
} catch (err) {
117113
console.error('Error on init when getting accounts', err);
118114
}
115+
globalContext.connected = true;
119116
}
120-
globalContext.connected = true;
121117
}

src/dapp-metadata.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const dappMetadata = {
2+
name: 'E2e Test Dapp',
3+
description: 'This is the E2e Test Dapp',
4+
url: 'https://metamask.github.io/test-dapp/',
5+
};
6+
7+
export default dappMetadata;

0 commit comments

Comments
 (0)