/
index.ts
330 lines (283 loc) · 9.26 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import EventEmitter from "events";
import { ExternalProvider, Web3Provider } from "@ethersproject/providers";
import { ethers } from "ethers";
import { networks, UnicrowNetwork } from "./networks";
import { CHAIN_ID } from "../helpers";
import { DefaultNetwork, IGenericTransactionCallbacks } from "typing";
import { config } from "../config";
let currentWallet: string | null = null;
let accountChangedListener: EventEmitter | null = null;
let chainChangedListener: EventEmitter | null = null;
let _onChangeNetworkCallbacks: Array<(networkId: number) => void> = [];
let _onChangeWalletCallbacks: Array<(currentWallet: string) => void> = [];
const handleAccountsChanged = (accounts: string[]) => {
if (currentWallet === accounts[0]) {
return;
}
if (accounts.length === 0) {
// MetaMask is locked or the user has not connected any accounts
currentWallet = null;
} else {
currentWallet = accounts[0];
}
_onChangeWalletCallbacks.length > 0 &&
_onChangeWalletCallbacks.forEach(
(callback: (currentWallet: string) => void) => callback(currentWallet),
);
};
const handleChainChanged = (networkId: string) => {
const _networkId = Number(networkId);
_onChangeNetworkCallbacks.length > 0 &&
_onChangeNetworkCallbacks.forEach(
(callback: (networkId: number) => void) => callback(_networkId),
);
};
const registerChainChangedListener = () => {
const ethereum = checkIsWalletInstalled();
if (ethereum && !chainChangedListener) {
chainChangedListener = ethereum!.on("chainChanged", (networkId) => {
console.info("chainChanged", networkId);
handleChainChanged(networkId);
});
}
};
const registerAccountChangedListener = () => {
const ethereum = checkIsWalletInstalled();
if (ethereum && !accountChangedListener) {
accountChangedListener = ethereum!.on(
"accountsChanged",
(accounts: any) => {
handleAccountsChanged(accounts);
},
);
}
};
/**
* Connects user's web3 wallet
*
* @returns address of the connected account
*/
export const connect = async (): Promise<string | null> => {
if (!currentWallet) {
registerAccountChangedListener();
const ethereum = checkIsWalletInstalled();
if (!ethereum) return null;
const _accounts = await ethereum.request({
method: "eth_requestAccounts",
});
handleAccountsChanged(_accounts);
if (_accounts && _accounts.length > 0) {
currentWallet = _accounts[0];
return _accounts[0];
}
return null;
}
return currentWallet;
};
/**
* Asks user's web3 wallet to switch to a selected network
*
* @param name - Name of one of the configured networks ('arbitrum', 'development', or 'goerli' in standard SDK installation)
* @returns Name of the network that the wallet was switched to. Null if no wallet is installed
* @throws Error if the user rejected adding or switching to the network
*/
export const switchNetwork = async (name: DefaultNetwork) => {
if (!checkIsWalletInstalled()) return null;
const { chainName, rpcUrls, chainId, nativeCurrency, blockExplorerUrls } =
networks[name];
registerAccountChangedListener();
const addParams: any = {
chainId: ethers.utils.hexValue(chainId),
chainName,
rpcUrls,
nativeCurrency,
blockExplorerUrls,
};
const switchParams: any = { chainId: addParams.chainId };
/**
* one could think that if one of the following two rpc methods fail, the code should continue within the catch blocks.
* which of course it does, **but**: Metamask still throws an error in that situation. in order to prevent that,
* we're calling both rpc methods in individual try-blocks instead of how it is described in their docs here:
*
* https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
*/
try {
// check if the chain to connect to is installed
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [switchParams], // chainId must be in hexadecimal numbers
});
} catch (error) {
// This error code indicates that the chain has not been added to MetaMask
// if it is not, then install it into the user MetaMask
if (error.code === 4902) {
try {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [addParams],
});
} catch (addError) {
throw new Error("User rejected network addition");
}
}
throw new Error("User rejected network switch");
}
const connected = await getNetwork();
if (connected.chainId === chainId) {
config({ defaultNetwork: name });
}
return name;
};
/**
* If non-default network is connected and if auto-switch is configured globally or requested by "force" parameter,
* switch wallet to the default network
*
* @param force - True to force switching to the default network
* @throws Unsupported network error if the user is on incorrect network, and neither global settings nor the parameter requires the switch
*/
export const autoSwitchNetwork = async (
callbacks?: IGenericTransactionCallbacks,
force: boolean = false,
) => {
const isCorrectNetwork = await isCorrectNetworkConnected();
if (!isCorrectNetwork) {
if (globalThis.autoSwitchNetwork || force) {
await switchNetwork(globalThis.defaultNetwork.name);
callbacks && callbacks.switchingNetwork && callbacks.switchingNetwork();
} else {
throw new Error("Unsupported network");
}
}
};
/**
* Get parameters of the network that the wallet is connected to
*
* @returns Network parameters
*/
export const getNetwork = async (): Promise<ethers.providers.Network> => {
const provider = await getWeb3Provider();
if (provider === null) {
return {
chainId: 0,
name: "unknown",
};
}
let network = await provider.getNetwork();
if (network.chainId === CHAIN_ID.development) {
network = {
...network,
name: "development",
};
}
return network;
};
/**
* Get list of networks supported by the configuration
*
* @returns List and parameters of all configured networks
*/
export const getSupportedNetworks: () => {
[name: string]: UnicrowNetwork;
} = () => networks;
/**
* Checks, based on chainId comparison, if the wallet is connected to the default network
*
* @returns true/false if the wallet is connected to the default network
*/
export const isCorrectNetworkConnected = async (): Promise<boolean> => {
const network = await getNetwork();
return network.chainId === globalThis.defaultNetwork.chainId;
};
/**
* Checks, based on chainId comparison, if the wallet is connected to one of the networks supported by the configuration
*
* @returns true/false
*/
export const isSupportedNetworkConnected = async (): Promise<boolean> => {
const network = await getNetwork();
const currentNetwork = Object.values(networks).filter(
(n) => n.chainId === network.chainId,
);
return currentNetwork.length > 0;
};
/**
* Start listening to change in wallet connection and run the callback function if the account changes
*
* @param onChangeWalletCallback Function to be called if the user changes a connected account
*/
export const startListening = (
onChangeWalletCallback: (walletAddress: string | null) => void,
) => {
checkIsWalletInstalled();
_onChangeWalletCallbacks.push(onChangeWalletCallback);
registerAccountChangedListener();
};
/**
* Listen to whether the wallet switches to another network and run the provided callback if yes
*
* @param onChangeNetworkCallback Function to be called when the wallet switches to another network
*/
export const startListeningNetwork = (
onChangeNetworkCallback: (networkId: number) => void,
) => {
_onChangeNetworkCallbacks.push(onChangeNetworkCallback);
registerChainChangedListener();
};
/**
* Check if the app is listening to account or network change
*
* @returns true if at least one of the listeners is active
*/
export const isListening = (): boolean => {
return _onChangeWalletCallbacks.length > 0;
};
/**
* Stop listening to wallet account changes
*/
export const stopListening = () => {
_onChangeWalletCallbacks = [];
};
/**
* Stop listening to network switch
*/
export const stopListeningNetwork = () => {
_onChangeNetworkCallbacks = [];
};
export const getWeb3Provider = async (): Promise<Web3Provider> => {
// TODO: merge this with checkIsWalletInstalled
const ethereum = checkIsWalletInstalled();
return ethereum
? new ethers.providers.Web3Provider(
ethereum as unknown as ExternalProvider,
"any",
)
: null;
};
/**
* Check if a web3 wallet is installed
*
* @returns installed web3 provider or null if none
* @throws If this is not run in a browser
*/
export const checkIsWalletInstalled = () => {
if (typeof window === "undefined") {
throw new Error("Should run through Browser");
}
// Have to check the ethereum binding on the window object to see if it's installed
const { ethereum } = window;
try {
// is there a more agnostic way to check? I know otherwallets use isMetaMask too, but perhaps there are better flags
const check = ethereum.isMetaMask;
} catch (e) {
return null;
}
return ethereum;
};
/**
* Returns connected wallet account (Attempts to connect to the wallet if not connected)
* @returns Account address
*/
export const getWalletAccount = async () => {
await connect();
return currentWallet;
};