-
Notifications
You must be signed in to change notification settings - Fork 80
/
arc.ts
596 lines (518 loc) · 18 KB
/
arc.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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
import { NotificationStatus } from "@store/notifications/notifications.reducer";
import { getNetworkId, getNetworkName, targetedNetwork, targetNetworks, Networks } from "./lib/util";
import { settings } from "./settings";
import { Address, Arc } from "@daostack/arc.js";
import Web3Modal, { getProviderInfo, IProviderInfo } from "web3modal";
import { Observable } from "rxjs";
import gql from "graphql-tag";
const Web3 = require("web3");
const MAX_BATCH_QUERY = 1000;
/**
* This is only set after the user has selected a provider and enabled an account.
* It is like window.ethereum, but has not necessarily been injected as such.
*/
let selectedProvider: any;
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
let web3Modal: Web3Modal;
let initializedAccount: Address;
interface IDAO {
id: string;
name: string;
}
/**
* return the default Arc configuration given the execution environment
*/
export function getArcSettings(network?: Networks): any {
const networks = network || targetedNetwork();
const arcSettings = settings[networks];
return arcSettings;
}
/**
* Return the web3 in current use by Arc.
*/
function getWeb3(network: Networks): any {
const arc = window.arcs[network];
const web3 = arc ? arc.web3 : null;
return web3;
}
/**
* Return the default account in current use by Arc.
*/
async function _getCurrentAccountFromProvider(network: Networks, web3?: any): Promise<string> {
web3 = web3 || getWeb3(network as Networks);
if (!web3) {
return null;
}
const accounts = await web3.eth.getAccounts();
return accounts[0] ? accounts[0].toLowerCase() : null;
}
/**
* Returns an Arc instance by Netowrk.
* Throws an exception when Arc hasn't yet been initialized!
* @param {Networks} network
* @returns {Arc}
*/
export function getArc(network: Networks): Arc {
const arc = window.arcs[network];
if (!arc) {
throw Error(`window.arc is not defined for ${network} - please call initializeArc first`);
}
return arc;
}
/**
* Returns the arcs object
*/
export function getArcs(): any {
const arcs = window.arcs;
if (!arcs) {
throw Error("window.arcs is not defined - please call initializeArc first");
}
return arcs;
}
/**
* Returns daos object (which hold all daos from all networks)
*/
export function getDAOs(): any {
const daos = window.daos;
if (!daos) {
throw Error("window.daos is not defined - please call initializeArc first");
}
return daos;
}
/**
* Return currently-selected and fully-enabled web3Provider (an account can be presumed to exist).
*/
export function getWeb3Provider(): any | undefined {
return selectedProvider;
}
export async function getProviderNetworkName(provider?: any): Promise<string> {
provider = provider || selectedProvider;
if (!provider) { return null; }
const networkId = await getNetworkId(provider);
return getNetworkName(networkId);
}
/**
* Returns a IWeb3ProviderInfo when a provider has been selected and is fully available.
* Does not know about the default read-only providers.
*/
export function getWeb3ProviderInfo(provider?: any): IWeb3ProviderInfo {
provider = provider || selectedProvider;
return provider ? getProviderInfo(provider) : null;
}
export function providerHasConfigUi(provider?: any): any | undefined {
provider = provider || selectedProvider;
return provider && provider.isTorus;
}
async function getAllDaos(arc: Arc): Promise<Array<IDAO>> {
const allDaos = [];
let daos = [];
let skip = 0;
do {
const query = gql`
query AllDaos {
daos(first: ${MAX_BATCH_QUERY} skip: ${skip * MAX_BATCH_QUERY}) {
id
name
}
}
`;
const response = await arc.sendQuery(query, {});
daos = response.data.daos as Array<IDAO>;
if (daos.length > 0) {
allDaos.push(...daos);
}
skip++;
} while (daos.length === MAX_BATCH_QUERY);
return allDaos;
}
/**
* initialize Arc. Does not throw exceptions, returns boolean success.
* @param provider Optional web3Provider
*/
export async function initializeArc(network: Networks, provider?: any): Promise<boolean> {
let success = false;
let arc: any;
try {
const arcSettings = getArcSettings(network);
if (provider) {
arcSettings.web3Provider = provider;
} else {
provider = arcSettings.web3Provider;
}
const readonly = typeof provider === "string";
// if there is no existing arc, we create a new one
if (window.arcs[network]) {
arc = window.arcs[network];
arc.web3 = new Web3(provider);
}
else {
arc = new Arc(arcSettings);
}
let contractInfos;
try {
contractInfos = await arc.fetchContractInfos();
} catch (err) {
// eslint-disable-next-line no-console
console.error(`Error fetching contractinfos: ${err.message}`);
}
const daos = await getAllDaos(arc);
if (daos !== undefined) {
const daosMap: {[key in string]: string} = {};
for (const dao of daos) {
daosMap[dao.id] = dao.name;
}
window.daos[network] = daosMap;
} else {
// eslint-disable-next-line no-console
console.error(`Error fetching daos from: ${network}`);
}
success = !!contractInfos && !!daos;
if (success) {
initializedAccount = await _getCurrentAccountFromProvider(network, arc.web3);
if (!initializedAccount) {
// then something went wrong
// eslint-disable-next-line no-console
console.error(`Unable to obtain an account from the provider in ${network}`);
}
} else {
initializedAccount = null;
}
if (success) {
provider = arc.web3.currentProvider; // won't be a string, but the actual provider
// save for future reference
// eslint-disable-next-line require-atomic-updates
provider.__networkId = await getNetworkId(provider);
if (window.ethereum) {
// if this is metamask this should prevent a browser refresh when the network changes
window.ethereum.autoRefreshOnNetworkChange = false;
}
// eslint-disable-next-line no-console
console.log(`Connected Arc to ${await getNetworkName(provider.__networkId)}${readonly ? " (readonly)" : ""} `);
}
} catch (reason) {
// eslint-disable-next-line no-console
console.error(reason ? reason.message : "unknown error");
}
window.arcs[network] = success ? arc : null;
return success;
}
/**
* Checks if the web3 provider is set to the required network.
* Does not ensure we have access to the user's account.
* throws an Error if no provider or wrong provider
* @param provider web3Provider
* @return the expected network nameif not correct
*/
async function ensureCorrectNetwork(provider: any, network?: Networks): Promise<void> {
/**
* It is required that the provider be the correct one for the current platform
*/
const expectedNetworkNames = network?[network]: targetNetworks();
// TODO: we should not use the network NAME but the network ID to identify the network...
const networkName = await getProviderNetworkName(provider);
if (!expectedNetworkNames.includes(networkName as Networks)) {
if (expectedNetworkNames.includes("xdai")) {
// TODO: xdai is reporting network 'unknown (100)` , it seems
if (networkName === "unknown (100)") {
// we are fine, mayby
return;
}
}
// eslint-disable-next-line no-console
console.error(`connected to the wrong network, should be ${expectedNetworkNames} (instead of "${networkName}")`);
throw new Error(`Please connect your wallet provider to ${(expectedNetworkNames as Array<any>).join(" or ")}`);
}
}
const ACCOUNT_STORAGEKEY = "currentAddress";
export function cacheWeb3Info(account: Address): void {
if (account) {
localStorage.setItem(ACCOUNT_STORAGEKEY, account);
} else {
localStorage.removeItem(ACCOUNT_STORAGEKEY);
}
}
export function uncacheWeb3Info(accountToo = true): void {
if (accountToo) {
localStorage.removeItem(ACCOUNT_STORAGEKEY);
}
if (web3Modal) {
web3Modal.clearCachedProvider();
}
/**
* close is not yet a standard, but soon will be.
* Sadly closing the connection is the only way to clear the WalletConnect cache.
* But clearing its cache will ensure that
* the user can rescan a qrcode when changing WalletConnect provider.
*/
if (selectedProvider && selectedProvider.close) {
selectedProvider.close(); // no need to await
}
}
export function getCachedAccount(): Address | null {
return localStorage.getItem(ACCOUNT_STORAGEKEY);
}
export interface IEnableWalletProviderParams {
suppressNotifyOnSuccess?: boolean;
showNotification: any;
}
function inTesting(): boolean {
if (process.env.NODE_ENV === "development" && (global as any).inAlchemyTests) {
// in test mode, we have an unlocked ganache and we are not using any wallet
// eslint-disable-next-line no-console
console.log("not using any wallet, because we are in automated test");
selectedProvider = new Web3(settings.ganache.web3Provider);
return true;
}
return false;
}
/**
* Prompt user to select a web3Provider and enable their account.
* Initializes Arc with the newly-selected web3Provider.
* No-op if `selectedProvider` is already set (one can manually go to readonly mode to clear it)
* Side-effect is that `selectedProvider` will be set on success.
* @returns Throws exception on error.
*/
async function enableWeb3Provider(network?: Networks): Promise<void> {
if (selectedProvider) {
return;
}
let provider: any;
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
let _web3Modal: Web3ConnectModal;
if (!web3Modal) {
_web3Modal = new Web3Modal({
cacheProvider: true,
providerOptions: getArcSettings(network).web3ConnectProviderOptions,
});
// eslint-disable-next-line require-atomic-updates
web3Modal = _web3Modal;
} else {
_web3Modal = web3Modal;
}
let resolveOnClosePromise: () => void;
let rejectOnClosePromise: (reason?: any) => void;
const onClosePromise = new Promise(
(resolve: () => void, reject: (reason?: any) => void): any => {
resolveOnClosePromise = resolve;
rejectOnClosePromise = reject;
_web3Modal.on("close", (): any => {
return resolve();
});
});
_web3Modal.on("error", (error: Error): any => {
// eslint-disable-next-line no-console
console.error(`web3Connect closed on error: ${error ? error.message : "cancelled or unknown error"}`);
return rejectOnClosePromise(error);
});
_web3Modal.on("connect", (newProvider: any): any => {
provider = newProvider;
/**
* Because we won't receive the "close" event in this case, even though
* the window will have closed
*/
return resolveOnClosePromise();
});
try {
// note this will load from its cache, if present
_web3Modal.toggleModal();
// assuming reject will result in a throw exception caught below
await onClosePromise;
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Unable to connect to web3 provider: ${error ? error.message : "unknown error"}`);
throw new Error("Unable to connect to web3 provider");
}
if (!provider) {
// should only be cancelled, errors should have been handled above
// eslint-disable-next-line no-console
console.warn("uncaught error or user cancelled out");
return;
}
/**
* bail if provider is not correct for the current platform
*/
await ensureCorrectNetwork(provider, network);
/**
* now ensure that the user has connected to a network and enabled access to the account,
* whatever the provider requires....
*/
try {
// brings up the provider UI as needed
await provider.enable();
// eslint-disable-next-line no-console
console.log(`Connected to network provider ${getWeb3ProviderInfo(provider).name}`);
} catch (ex) {
// eslint-disable-next-line no-console
console.error(`Unable to enable provider: ${ex.message ? ex : "unknown error"}`);
throw new Error("Unable to enable provider");
}
if (!await initializeArc(await getNetworkName(provider.chainId), provider)) {
// eslint-disable-next-line no-console
console.error("Unable to initialize Arc");
throw new Error("Unable to initialize Arc");
}
// eslint-disable-next-line require-atomic-updates
selectedProvider = provider;
}
/**
* @return the current account address from Arc. Ignores any injected
* account unless Arc knows about the provider.
*/
async function getCurrentAccountFromProvider(): Promise<Address | null> {
if (!selectedProvider) {
/**
* though an account may actually be available via injection, we're not going
* to return it. The flow needs to start from a selected provider first,
* only then the current account.
*/
return null;
}
return _getCurrentAccountFromProvider(await getNetworkName(selectedProvider.chainId));
}
/**
* Logout, switch to readonly mode (effectively a logout).
* Clear caches as every case where we're manually logging out
* implies that the cache should be cleared
*/
export async function logout(showNotification?: any): Promise<boolean> {
let success = false;
uncacheWeb3Info();
if (selectedProvider) {
// clearing this, initializeArc will be made to use the default readonly web3Provider
const networkName = await getNetworkName(selectedProvider.chainId);
// eslint-disable-next-line require-atomic-updates
selectedProvider = undefined;
success = await initializeArc(networkName);
if (!success) {
const msg = `Unable to disconnect from : ${networkName}`;
if (showNotification) {
showNotification(NotificationStatus.Failure, msg);
} else {
alert(msg);
}
}
}
else {
success = true;
}
return success;
}
/**
* @returns whether we have a current account
*/
export function getAccountIsEnabled(): boolean {
/**
* easy proxy for the presence of an account. selectedProvider cannot be set without an account.
*/
return !!getWeb3Provider();
}
/**
* Load web3 wallet provider, first trying from cache, otherwise prompting.
* This is the only point of contact with the rest of the app for connecting to a wallet.
* App.tsx invokes `initializeArc` on startup just to get a readonly web3.
* @param options `IEnableWWalletProviderParams`
* @returns Promise of true on success
*/
export async function enableWalletProvider(options: IEnableWalletProviderParams, network: Networks): Promise<boolean> {
try {
if (inTesting()) {
return true;
}
/**
* If not MetaMask or other injected web3 and on ganache then try to connect to local ganache directly.
* Note we're going to ignore any injected web3 in favor of using our own preferred version of Web3.
*/
if (!selectedProvider && (targetedNetwork() === "ganache" && !(window as any).ethereum)) {
selectedProvider = new Web3(settings.ganache.web3Provider);
}
if (!selectedProvider) {
await enableWeb3Provider(network);
if (!selectedProvider) {
// something went wrong somewhere
throw new Error("Unable to connect to a wallet");
}
/**
* notify on success
*/
if (!options.suppressNotifyOnSuccess && options.showNotification) {
const web3ProviderInfo = getWeb3ProviderInfo();
options.showNotification(NotificationStatus.Success, `Connected to ${web3ProviderInfo.name}`);
}
} else {
/**
* Bail if provider is not correct for the current platform. The user might have redirected
* Metamask to a different network without us knowing. Just in that case, check here.
*/
try {
await ensureCorrectNetwork(selectedProvider, network);
} catch (ex) {
throw new Error(ex);
}
}
} catch (err) {
let msg: string;
msg = err ? err.message : "Unable to connect to the ethereum provider";
if (msg.match(/response has no error or result for request/g)) {
msg = "Unable to connect to ethereum provider, sorry :-(";
}
uncacheWeb3Info(false);
if (options.showNotification) {
options.showNotification(NotificationStatus.Failure, msg);
} else {
alert(msg);
}
return false;
}
return true;
}
// Polling is Evil!
// TO DO: See https://github.com/daostack/alchemy/issues/2295.
export function pollForAccountChanges(currentAccountAddress: Address | null, interval = 2000): Observable<Address> {
// eslint-disable-next-line no-console
console.log(`start polling for account changes from: ${currentAccountAddress}`);
return Observable.create((observer: any): () => void => {
let prevAccount = currentAccountAddress;
let running = false;
async function poll(): Promise<void> {
if (!running) {
running = true;
try {
await getCurrentAccountFromProvider()
.then(async (account: Address | null): Promise<void> => {
if (prevAccount !== account) {
if (account && initializedAccount && (account !== initializedAccount)) {
/**
* Handle when user changes account in MetaMask while already connected to Alchemy.
* Also handles how the Burner provider switches from a Fortmatic address to the
* burner address at the time of connecting.
*/
await initializeArc(await getNetworkName(selectedProvider.chainId), selectedProvider);
}
observer.next(account);
// eslint-disable-next-line require-atomic-updates
prevAccount = account;
}
})
// eslint-disable-next-line no-console
.catch((err): void => {console.error(err ? err.message : "unknown error"); });
} catch (ex) {
// eslint-disable-next-line no-console
console.error(ex ? ex.message : "unknown error");
}
finally {
running = false;
}
}
}
poll();
const timeout = setInterval(poll, interval);
return (): void => { clearTimeout(timeout); };
});
}
/**
* extension of the Web3Modal IProviderInfo
*/
export interface IWeb3ProviderInfo extends IProviderInfo {
}