/
index.ts
384 lines (324 loc) · 13 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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
import type { Networkish } from '@ethersproject/networks';
import { Web3Provider } from '@ethersproject/providers';
import { createWeb3ReactStoreAndActions } from '@disco3/store';
import type {
Actions,
Connector,
Web3ReactState,
Web3ReactStore,
} from '@disco3/types';
import { useEffect, useMemo, useState } from 'react';
import type { EqualityChecker, UseBoundStore } from 'zustand';
import create from 'zustand';
export type Web3ReactHooks = ReturnType<typeof getStateHooks> &
ReturnType<typeof getDerivedHooks> &
ReturnType<typeof getAugmentedHooks>
export type Web3ReactSelectedHooks = ReturnType<typeof getSelectedConnector>
export type Web3ReactPriorityHooks = ReturnType<typeof getPriorityConnector>
/**
* Wraps the initialization of a `connector`. Creates a zustand `store` with `actions` bound to it, and then passes
* these to the connector as specified in `f`. Also creates a variety of `hooks` bound to this `store`.
*
* @typeParam T - The type of the `connector` returned from `f`.
* @param f - A function which is called with `actions` bound to the returned `store`.
* @param allowedChainIds - An optional array of chainIds which the `connector` may connect to. If the `connector` is
* connected to a chainId which is not allowed, a ChainIdNotAllowedError error will be reported.
* If this argument is unspecified, the `connector` may connect to any chainId.
* @returns [connector, hooks, store] - The initialized connector, a variety of hooks, and a zustand store.
*/
export function initializeConnector<T extends Connector>(
f: (actions: Actions) => T,
allowedChainIds?: number[]
): [T, Web3ReactHooks, Web3ReactStore] {
const [store, actions] = createWeb3ReactStoreAndActions(allowedChainIds)
const connector = f(actions)
const useConnector = create<Web3ReactState>(store)
const stateHooks = getStateHooks(useConnector)
const derivedHooks = getDerivedHooks(stateHooks)
const augmentedHooks = getAugmentedHooks(connector, stateHooks, derivedHooks)
return [connector, { ...stateHooks, ...derivedHooks, ...augmentedHooks }, store]
}
function computeIsActive({ chainId, accounts, activating, error }: Web3ReactState) {
return Boolean(chainId && accounts && !activating && !error)
}
/**
* Creates a variety of convenience `hooks` that return data associated with a particular passed connector.
*
* @param initializedConnectors - Two or more [connector, hooks] arrays, as returned from initializeConnector.
* @returns hooks - A variety of convenience hooks that wrap the hooks returned from initializeConnector.
*/
export function getSelectedConnector(...initializedConnectors: [Connector, Web3ReactHooks][]) {
function getIndex(connector: Connector) {
const index = initializedConnectors.findIndex(([initializedConnector]) => connector === initializedConnector)
if (index === -1) throw new Error('Connector not found')
return index
}
// the following code calls hooks in a map a lot, which violates the eslint rule.
// this is ok, though, because initializedConnectors never changes, so the same hooks are called each time
function useSelectedChainId(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useChainId }]) => useChainId())
return values[getIndex(connector)]
}
function useSelectedAccounts(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useAccounts }]) => useAccounts())
return values[getIndex(connector)]
}
function useSelectedIsActivating(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useIsActivating }]) => useIsActivating())
return values[getIndex(connector)]
}
function useSelectedError(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useError }]) => useError())
return values[getIndex(connector)]
}
function useSelectedAccount(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useAccount }]) => useAccount())
return values[getIndex(connector)]
}
function useSelectedIsActive(connector: Connector) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useIsActive }]) => useIsActive())
return values[getIndex(connector)]
}
function useSelectedProvider(connector: Connector, network?: Networkish) {
const index = getIndex(connector)
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useProvider }], i) => useProvider(network, i === index))
return values[index]
}
function useSelectedENSNames(connector: Connector, provider: Web3Provider | undefined) {
const index = getIndex(connector)
const values = initializedConnectors.map(([, { useENSNames }], i) =>
// eslint-disable-next-line react-hooks/rules-of-hooks
useENSNames(i === index ? provider : undefined)
)
return values[index]
}
function useSelectedENSName(connector: Connector, provider: Web3Provider | undefined) {
const index = getIndex(connector)
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useENSName }], i) => useENSName(i === index ? provider : undefined))
return values[index]
}
function useSelectedWeb3React(connector: Connector, provider: Web3Provider | undefined) {
const index = getIndex(connector)
const values = initializedConnectors.map(([, { useWeb3React }], i) =>
// eslint-disable-next-line react-hooks/rules-of-hooks
useWeb3React(i === index ? provider : undefined)
)
return values[index]
}
return {
useSelectedChainId,
useSelectedAccounts,
useSelectedIsActivating,
useSelectedError,
useSelectedAccount,
useSelectedIsActive,
useSelectedProvider,
useSelectedENSNames,
useSelectedENSName,
useSelectedWeb3React,
}
}
/**
* Creates a variety of convenience `hooks` that return data associated with the first of the `initializedConnectors`
* that is active.
*
* @param initializedConnectors - Two or more [connector, hooks] arrays, as returned from initializeConnector.
* @returns hooks - A variety of convenience hooks that wrap the hooks returned from initializeConnector.
*/
export function getPriorityConnector(...initializedConnectors: [Connector, Web3ReactHooks][]) {
const {
useSelectedChainId,
useSelectedAccounts,
useSelectedIsActivating,
useSelectedError,
useSelectedAccount,
useSelectedIsActive,
useSelectedProvider,
useSelectedENSNames,
useSelectedENSName,
useSelectedWeb3React,
} = getSelectedConnector(...initializedConnectors)
function usePriorityConnector() {
// eslint-disable-next-line react-hooks/rules-of-hooks
const values = initializedConnectors.map(([, { useIsActive }]) => useIsActive())
const index = values.findIndex((isActive) => isActive)
return initializedConnectors[index === -1 ? 0 : index][0]
}
function usePriorityChainId() {
return useSelectedChainId(usePriorityConnector())
}
function usePriorityAccounts() {
return useSelectedAccounts(usePriorityConnector())
}
function usePriorityIsActivating() {
return useSelectedIsActivating(usePriorityConnector())
}
function usePriorityError() {
return useSelectedError(usePriorityConnector())
}
function usePriorityAccount() {
return useSelectedAccount(usePriorityConnector())
}
function usePriorityIsActive() {
return useSelectedIsActive(usePriorityConnector())
}
function usePriorityProvider(network?: Networkish) {
return useSelectedProvider(usePriorityConnector(), network)
}
function usePriorityENSNames(provider: Web3Provider | undefined) {
return useSelectedENSNames(usePriorityConnector(), provider)
}
function usePriorityENSName(provider: Web3Provider | undefined) {
return useSelectedENSName(usePriorityConnector(), provider)
}
function usePriorityWeb3React(provider: Web3Provider | undefined) {
return useSelectedWeb3React(usePriorityConnector(), provider)
}
return {
useSelectedChainId,
useSelectedAccounts,
useSelectedIsActivating,
useSelectedError,
useSelectedAccount,
useSelectedIsActive,
useSelectedProvider,
useSelectedENSNames,
useSelectedENSName,
useSelectedWeb3React,
usePriorityConnector,
usePriorityChainId,
usePriorityAccounts,
usePriorityIsActivating,
usePriorityError,
usePriorityAccount,
usePriorityIsActive,
usePriorityProvider,
usePriorityENSNames,
usePriorityENSName,
usePriorityWeb3React,
}
}
const CHAIN_ID = (state: Web3ReactState) => state.chainId
const ACCOUNTS = (state: Web3ReactState) => state.accounts
const ACCOUNTS_EQUALITY_CHECKER: EqualityChecker<Web3ReactState['accounts']> = (oldAccounts, newAccounts) =>
(oldAccounts === undefined && newAccounts === undefined) ||
(oldAccounts !== undefined &&
oldAccounts.length === newAccounts?.length &&
oldAccounts.every((oldAccount, i) => oldAccount === newAccounts[i]))
const ACTIVATING = (state: Web3ReactState) => state.activating
const ERROR = (state: Web3ReactState) => state.error
function getStateHooks(useConnector: UseBoundStore<Web3ReactState>) {
function useChainId(): Web3ReactState['chainId'] {
return useConnector(CHAIN_ID)
}
function useAccounts(): Web3ReactState['accounts'] {
return useConnector(ACCOUNTS, ACCOUNTS_EQUALITY_CHECKER)
}
function useIsActivating(): Web3ReactState['activating'] {
return useConnector(ACTIVATING)
}
function useError(): Web3ReactState['error'] {
return useConnector(ERROR)
}
return { useChainId, useAccounts, useIsActivating, useError }
}
function getDerivedHooks({ useChainId, useAccounts, useIsActivating, useError }: ReturnType<typeof getStateHooks>) {
function useAccount(): string | undefined {
return useAccounts()?.[0]
}
function useIsActive(): boolean {
const chainId = useChainId()
const accounts = useAccounts()
const activating = useIsActivating()
const error = useError()
return computeIsActive({
chainId,
accounts,
activating,
error,
})
}
return { useAccount, useIsActive }
}
function useENS(provider?: Web3Provider, accounts?: string[]): (string | null)[] | undefined {
const [ENSNames, setENSNames] = useState<(string | null)[] | undefined>()
useEffect(() => {
if (provider && accounts?.length) {
let stale = false
Promise.all(accounts.map((account) => provider.lookupAddress(account)))
.then((ENSNames) => {
if (!stale) {
setENSNames(ENSNames)
}
})
.catch((error) => {
console.debug('Could not fetch ENS names', error)
})
return () => {
stale = true
setENSNames(undefined)
}
}
}, [provider, accounts])
return ENSNames
}
function getAugmentedHooks<T extends Connector>(
connector: T,
{ useChainId, useAccounts, useError }: ReturnType<typeof getStateHooks>,
{ useAccount, useIsActive }: ReturnType<typeof getDerivedHooks>
) {
function useProvider(network?: Networkish, enabled = true): Web3Provider | undefined {
const isActive = useIsActive()
const chainId = useChainId()
const accounts = useAccounts()
// trigger the dynamic import on mount
const [providers, setProviders] = useState<{ Web3Provider: typeof Web3Provider } | undefined>(undefined)
useEffect(() => {
import('@ethersproject/providers').then(setProviders).catch(() => {
console.debug('@ethersproject/providers not available')
})
}, [])
return useMemo(() => {
// we use chainId and accounts to re-render in case connector.provider changes in place
if (providers && enabled && isActive && chainId && accounts && connector.provider) {
return new providers.Web3Provider(connector.provider, network)
}
}, [providers, enabled, isActive, chainId, accounts, network])
}
function useENSNames(provider: Web3Provider | undefined): (string | null)[] | undefined {
const accounts = useAccounts()
return useENS(provider, accounts)
}
function useENSName(provider: Web3Provider | undefined): (string | null) | undefined {
const account = useAccount()
const accounts = useMemo(() => (account === undefined ? undefined : [account]), [account])
return useENS(provider, accounts)?.[0]
}
// for backwards compatibility only
function useWeb3React(provider: Web3Provider | undefined) {
const chainId = useChainId()
const account = useAccount()
const error = useError()
const isActive = useIsActive()
return useMemo(
() => ({
connector,
library: provider,
chainId,
account,
active: isActive,
error,
}),
[provider, chainId, account, isActive, error]
)
}
return { useProvider, useENSNames, useENSName, useWeb3React }
}