Skip to content

Commit 15ce673

Browse files
wenfixffmcgee725jiexiadonesky1
authored
feat: wagmi connector (#31)
* draft new metamask connector and port wagmi's playground * update connector * update connect response * fix: make sure initial chain id matches permitted chain id * refactor: properly type wallet_getSession response from attemptSessionRecory + minor docs update * Fix add/switchChain not prompting for deeplink (#30) * WIP * WIP * fix build * v broken * a bit less broken * fix accountsChanged * clear cache on disconnect * remove notificationQueue. Attempt to get cached eth_accounts and eth_chainId * Fix onConnect and disconnect events? Not sure about this one * fix: de-parametrize TransportResponse for fixing build * add responses to the default transport * Add opendeeplink method in core --------- Co-authored-by: Alex Donesky <[email protected]> Co-authored-by: ffmcgee <[email protected]> Co-authored-by: Alex Mendonca <[email protected]> * enable requesting eth accounts without specifying and with an existing session * update connect * fix transport type * fix typedoc on connect-evm * add remove listener to event emitter * type fixes * update connect-multichain typedoc * add conditional logger to setup options * update changelog * change root build command to only build public packages * update wagmi readme * fix fallback addEthereumChain when switching fails * clean-up addEthereumChain * sync yarn.lock * sync wagmi connector * changelog * revert connect-multichain version change * format * format changelog * fix dts build on connect evm * supress source map warnings on multichain-react-playground * remove non-dev facing changes * add brackets and remove only fetch provider when needed in onDisconnect * remove unnecessary provider error on switchChain * revert connect-multichain versions * simplify withCapabilities parameter usage * different accounts check * always call wallet_switchEthereumChain when using default transport * add once and listenerCount to internal EventEmitter * remove MinimalEventEmitter type * stop emitting chainChanged when the chain hasnt changed * make getProvider sync * update changelog * simplify transportType getter * add comment about onAccountsChanged disconnect * add dapp name to wagmi playground app * add wallet_requestPermissions button * restore multichain-api-client 0.8.1 * set response * fix setResponse --------- Co-authored-by: ffmcgee <[email protected]> Co-authored-by: jiexi <[email protected]> Co-authored-by: Alex Donesky <[email protected]>
1 parent 0303d1a commit 15ce673

File tree

35 files changed

+4881
-178
lines changed

35 files changed

+4881
-178
lines changed

integrations/wagmi/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
src/generated.ts
2+
3+
# Logs
4+
logs
5+
*.log
6+
npm-debug.log*
7+
yarn-debug.log*
8+
yarn-error.log*
9+
pnpm-debug.log*
10+
lerna-debug.log*
11+
12+
node_modules
13+
dist
14+
dist-ssr
15+
*.local
16+
17+
# Editor directories and files
18+
.vscode/*
19+
!.vscode/extensions.json
20+
.idea
21+
.DS_Store
22+
*.suo
23+
*.ntvs*
24+
*.njsproj
25+
*.sln
26+
*.sw?

integrations/wagmi/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Wagmi Demo App
2+
3+
A reference implementation of [Metamask's Wagmi connector](https://github.com/wevm/wagmi/blob/main/packages/connectors/src/metaMask.ts) and a port of [Wagmi's React Playground](https://github.com/wevm/wagmi/tree/main/playgrounds/vite-react).
4+
5+
## Setup
6+
7+
> [!IMPORTANT]
8+
> Follow the steps on the root [README](../../README.md) first.
9+
10+
- `yarn dev` to run the app in localhost.
11+
- `yarn dev --host` to run and listen on all addresses, including LAN and public IP (useful for mobile physical devices)

integrations/wagmi/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite React</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import {
2+
type AddEthereumChainParameter,
3+
createMetamaskConnectEVM,
4+
type MetamaskConnectEVM,
5+
} from '@metamask/connect-evm';
6+
7+
import {
8+
ChainNotConfiguredError,
9+
createConnector,
10+
ProviderNotFoundError,
11+
} from '@wagmi/core';
12+
13+
import type { OneOf } from '@wagmi/core/internal';
14+
15+
import {
16+
type EIP1193Provider,
17+
getAddress,
18+
type ProviderConnectInfo,
19+
ResourceUnavailableRpcError,
20+
type RpcError,
21+
SwitchChainError,
22+
UserRejectedRequestError,
23+
} from 'viem';
24+
25+
type CreateMetamaskConnectEVMParameters = Parameters<
26+
typeof createMetamaskConnectEVM
27+
>[0];
28+
29+
const DEFAULT_CHAIN_ID = 1;
30+
31+
export type MetaMaskParameters = {
32+
dapp?: CreateMetamaskConnectEVMParameters['dapp'] | undefined;
33+
} & OneOf<
34+
| {
35+
/* Shortcut to connect and sign a message */
36+
connectAndSign?: string | undefined;
37+
}
38+
| {
39+
// TODO: Strongly type `method` and `params`
40+
/* Allow `connectWith` any rpc method */
41+
connectWith?: { method: string; params: unknown[] } | undefined;
42+
}
43+
>;
44+
45+
metaMask.type = 'metaMask' as const;
46+
export function metaMask(parameters: MetaMaskParameters = {}) {
47+
type Provider = EIP1193Provider;
48+
type Properties = {
49+
onConnect(connectInfo: ProviderConnectInfo): void;
50+
onDisplayUri(uri: string): void;
51+
};
52+
53+
let metamask: MetamaskConnectEVM;
54+
55+
return createConnector<Provider, Properties>((config) => ({
56+
id: 'metaMaskSDK',
57+
name: 'MetaMask',
58+
rdns: ['io.metamask', 'io.metamask.mobile'],
59+
type: metaMask.type,
60+
async setup() {
61+
const supportedNetworks = Object.fromEntries(
62+
config.chains.map((chain) => [
63+
`eip155:${chain.id}`,
64+
chain.rpcUrls.default?.http[0],
65+
]),
66+
);
67+
68+
// TODO: check if we need to support other parameters
69+
metamask = await createMetamaskConnectEVM({
70+
dapp: parameters.dapp ?? {},
71+
eventHandlers: {
72+
accountsChanged: this.onAccountsChanged.bind(this),
73+
chainChanged: this.onChainChanged.bind(this),
74+
connect: this.onConnect.bind(this),
75+
disconnect: this.onDisconnect.bind(this),
76+
},
77+
api: {
78+
supportedNetworks,
79+
},
80+
});
81+
},
82+
83+
async connect<withCapabilities extends boolean = false>(parameters?: {
84+
chainId?: number | undefined;
85+
isReconnecting?: boolean | undefined;
86+
withCapabilities?: withCapabilities | boolean | undefined;
87+
}) {
88+
const chainId = parameters?.chainId ?? DEFAULT_CHAIN_ID;
89+
const withCapabilities = parameters?.withCapabilities;
90+
91+
// TODO: Add connectAndSign and connectWith support, including events
92+
93+
try {
94+
const result = await metamask.connect({
95+
chainId,
96+
account: undefined,
97+
});
98+
99+
return {
100+
accounts: (withCapabilities
101+
? result.accounts.map((account) => ({
102+
address: account,
103+
capabilities: {},
104+
}))
105+
: result.accounts) as never,
106+
chainId: result.chainId ?? chainId,
107+
};
108+
} catch (err) {
109+
const error = err as RpcError;
110+
if (error.code === UserRejectedRequestError.code) {
111+
throw new UserRejectedRequestError(error);
112+
}
113+
if (error.code === ResourceUnavailableRpcError.code) {
114+
throw new ResourceUnavailableRpcError(error);
115+
}
116+
throw error;
117+
}
118+
},
119+
120+
async disconnect() {
121+
return metamask.disconnect();
122+
},
123+
124+
async getAccounts() {
125+
return metamask.accounts;
126+
},
127+
128+
async getChainId() {
129+
const chainId = await metamask.getChainId();
130+
if (chainId) {
131+
return Number(chainId);
132+
}
133+
// Fallback to requesting chainId from provider if SDK doesn't return it
134+
const provider = await this.getProvider();
135+
136+
const hexChainId = await provider.request({ method: 'eth_chainId' });
137+
return Number(hexChainId);
138+
},
139+
140+
async getProvider() {
141+
const provider = await metamask.getProvider();
142+
if (!provider) {
143+
throw new ProviderNotFoundError();
144+
}
145+
// Provider type-mismatch because Metamask uses tuples,
146+
// whereas viem uses direct parameters.
147+
// This is safe because both providers implement the same runtime interface
148+
// (on, removeListener, request); only the TypeScript signatures differ.
149+
// TODO: potential improvement here to avoid cast?
150+
return provider as unknown as Provider;
151+
},
152+
153+
async isAuthorized() {
154+
const accounts = await this.getAccounts();
155+
return accounts.length > 0;
156+
},
157+
158+
async switchChain({ addEthereumChainParameter, chainId }) {
159+
const chain = config.chains.find((x) => x.id === chainId);
160+
161+
if (!chain) {
162+
throw new SwitchChainError(new ChainNotConfiguredError());
163+
}
164+
165+
const rpcUrls = addEthereumChainParameter?.rpcUrls
166+
? [...addEthereumChainParameter.rpcUrls]
167+
: chain.rpcUrls.default?.http
168+
? [...chain.rpcUrls.default.http]
169+
: undefined;
170+
171+
const blockExplorerUrls = addEthereumChainParameter?.blockExplorerUrls
172+
? [...addEthereumChainParameter.blockExplorerUrls]
173+
: chain.blockExplorers?.default.url
174+
? [chain.blockExplorers.default.url]
175+
: undefined;
176+
177+
const chainConfiguration: AddEthereumChainParameter = {
178+
chainId: `0x${chainId.toString(16)}`,
179+
rpcUrls,
180+
nativeCurrency:
181+
addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency,
182+
chainName: addEthereumChainParameter?.chainName ?? chain.name,
183+
blockExplorerUrls,
184+
iconUrls: addEthereumChainParameter?.iconUrls,
185+
};
186+
187+
try {
188+
await metamask.switchChain({ chainId, chainConfiguration });
189+
return chain;
190+
} catch (err) {
191+
const error = err as RpcError;
192+
193+
if (error.code === UserRejectedRequestError.code) {
194+
throw new UserRejectedRequestError(error);
195+
}
196+
197+
throw new SwitchChainError(error);
198+
}
199+
},
200+
201+
async onAccountsChanged(accounts) {
202+
// TODO: verify if this is needed or if we can just rely on the
203+
// existing disconnect event instead
204+
// Disconnect if there are no accounts
205+
if (accounts.length === 0) {
206+
this.onDisconnect();
207+
return;
208+
}
209+
// Regular change event
210+
211+
config.emitter.emit('change', {
212+
accounts: accounts.map((account) => getAddress(account)),
213+
});
214+
},
215+
216+
async onChainChanged(chain) {
217+
const chainId = Number(chain);
218+
config.emitter.emit('change', { chainId });
219+
},
220+
221+
async onConnect(connectInfo) {
222+
const accounts = await this.getAccounts();
223+
if (accounts.length === 0) return;
224+
225+
const chainId = Number(connectInfo.chainId);
226+
config.emitter.emit('connect', { accounts, chainId });
227+
},
228+
229+
async onDisconnect(error?: RpcError) {
230+
// If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting
231+
// https://github.com/MetaMask/providers/pull/120
232+
if (error && (error as unknown as RpcError<1013>).code === 1013) {
233+
const provider = await this.getProvider();
234+
if (provider && (await this.getAccounts()).length > 0) return;
235+
}
236+
237+
config.emitter.emit('disconnect');
238+
},
239+
240+
async onDisplayUri(uri) {
241+
config.emitter.emit('message', { type: 'display_uri', data: uri });
242+
},
243+
}));
244+
}

integrations/wagmi/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "wagmi-demo",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "tsc && vite build",
7+
"dev": "vite",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"@metamask/connect-evm": "workspace:^",
12+
"@tanstack/query-sync-storage-persister": "5.0.5",
13+
"@tanstack/react-query": ">=5.45.1",
14+
"@tanstack/react-query-persist-client": "5.0.5",
15+
"@wagmi/core": "^2.22.1",
16+
"idb-keyval": "^6.2.1",
17+
"react": ">=18.3.1",
18+
"react-dom": ">=18.3.1",
19+
"viem": "2.*",
20+
"wagmi": "^2.19.2"
21+
},
22+
"devDependencies": {
23+
"@tanstack/react-query-devtools": "5.0.5",
24+
"@types/react": ">=18.3.1",
25+
"@types/react-dom": ">=18.3.0",
26+
"@vitejs/plugin-react": "^4.3.3",
27+
"@wagmi/cli": "^2.3.2",
28+
"buffer": "^6.0.3",
29+
"vite": "^5.4.20"
30+
}
31+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "vite-react",
3+
"description": "vite-react playground",
4+
"iconPath": "favicon.ico"
5+
}

0 commit comments

Comments
 (0)