@etherplay/connect 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,898 @@
1
+ import type {AlchemyMechanism, OriginAccount} from '@etherplay/alchemy';
2
+ import {writable} from 'svelte/store';
3
+ import {createPopupLauncher, type PopupPromise} from './popup.js';
4
+ import type {EIP1193ChainId, EIP1193WindowWalletProvider} from 'eip-1193';
5
+ import {
6
+ fromEntropyKeyToMnemonic,
7
+ fromMnemonicToFirstAccount,
8
+ fromSignatureToKey,
9
+ originKeyMessage,
10
+ originPublicKeyPublicationMessage,
11
+ } from '@etherplay/alchemy';
12
+ import {hashMessage} from './utils.js';
13
+ import {createProvider} from './provider.js';
14
+
15
+ export {fromEntropyKeyToMnemonic, originPublicKeyPublicationMessage, originKeyMessage};
16
+ export type {OriginAccount};
17
+
18
+ export type PopupSettings = {
19
+ walletHost: string;
20
+ mechanism: AlchemyMechanism;
21
+ // extraParams?: Record<string, string>;
22
+ };
23
+
24
+ export type WalletMechanism<WalletName extends string | undefined, Address extends `0x${string}` | undefined> = {
25
+ type: 'wallet';
26
+ } & (WalletName extends undefined ? {name?: undefined} : {name: WalletName}) &
27
+ (Address extends undefined ? {address?: undefined} : {address: Address});
28
+
29
+ export type Mechanism = AlchemyMechanism | WalletMechanism<string | undefined, `0x${string}` | undefined>;
30
+
31
+ export type FullfilledMechanism = AlchemyMechanism | WalletMechanism<string, `0x${string}`>;
32
+
33
+ export type Connection = {
34
+ // The connection can have an error in every state.
35
+ // a banner or other mechanism to show error should be used.
36
+ // error should be dismissable
37
+ error?: {message: string; cause?: any};
38
+ // wallets represent the web3 wallet installed on the user browser
39
+ wallets: EIP6963ProviderDetail[];
40
+ } & ( // loading can be true initially as the system will try to auto-login and fetch installed web3 wallet // Start in Idle
41
+ | {
42
+ step: 'Idle';
43
+ loading: boolean;
44
+ }
45
+ // It can then end up in MechanismToChoose if no specific connection mechanism was chosen upon clicking "connect"
46
+ | {
47
+ step: 'MechanismToChoose';
48
+ }
49
+ // if a social/email login mechanism was chose, a popup will be launched
50
+ // popupClosed can be true and this means the popup has been closed and the user has to cancel the process to continue further
51
+ | {
52
+ step: 'PopupLaunched';
53
+ popupClosed: boolean;
54
+ mechanism: AlchemyMechanism;
55
+ }
56
+ // If the user has chosen to use web3-wallet there might be multi-choice for it
57
+ | {
58
+ step: 'WalletToChoose';
59
+ mechanism: WalletMechanism<undefined, undefined>;
60
+ }
61
+ // Once a user has chosen a wallet, the system will try to connect to it
62
+ | {
63
+ step: 'WaitingForWalletConnection';
64
+ mechanism: WalletMechanism<string, undefined>;
65
+ }
66
+ // Once the wallet is connected, the system will need a signature
67
+ // this state represent the fact and require another user interaction to request the signature
68
+ | {
69
+ step: 'NeedWalletSignature';
70
+ mechanism: WalletMechanism<string, `0x${string}`>;
71
+ }
72
+ // This state is triggered once the signature is requested, the user will have to confirm with its wallet
73
+ | {
74
+ step: 'WaitingForSignature';
75
+ mechanism: WalletMechanism<string, `0x${string}`>;
76
+ }
77
+ // Finally the user is fully signed in
78
+ // walletAccountChanged if set, represent the fact that the user has changed its web3-wallet accounnt.
79
+ // a notification could be shown to the user so that he can switch the app to use that other account.
80
+ | {
81
+ step: 'SignedIn';
82
+ mechanism: FullfilledMechanism;
83
+ account: OriginAccount;
84
+ wallet?: {
85
+ provider: EIP1193WindowWalletProvider;
86
+ accountChanged?: `0x${string}`;
87
+ chainId: string;
88
+ invalidChainId: boolean;
89
+ switchingChain: 'addingChain' | 'switchingChain' | false;
90
+ };
91
+ }
92
+ );
93
+
94
+ interface EIP6963ProviderInfo {
95
+ uuid: string;
96
+ name: string;
97
+ icon: string;
98
+ rdns: string;
99
+ }
100
+
101
+ interface EIP6963ProviderDetail {
102
+ info: EIP6963ProviderInfo;
103
+ provider: EIP1193WindowWalletProvider;
104
+ }
105
+
106
+ export interface EIP6963AnnounceProviderEvent extends CustomEvent {
107
+ type: 'eip6963:announceProvider';
108
+ detail: EIP6963ProviderDetail;
109
+ }
110
+
111
+ const storageAccountKey = '__origin_account';
112
+ export function createConnection(settings: {
113
+ walletHost: string;
114
+ autoConnect?: boolean;
115
+ node: {url: string; chainId: string; prioritizeWalletProvider?: boolean; requestsPerSecond?: number};
116
+ }) {
117
+ const alwaysOnChainId = settings.node.chainId;
118
+ const alwaysOnProvider = createProvider({
119
+ endpoint: settings.node.url,
120
+ chainId: settings.node.chainId,
121
+ prioritizeWalletProvider: settings.node.prioritizeWalletProvider,
122
+ requestsPerSecond: settings.node.requestsPerSecond,
123
+ });
124
+ let autoConnect = true;
125
+ if (typeof settings.autoConnect !== 'undefined') {
126
+ autoConnect = settings.autoConnect;
127
+ }
128
+
129
+ let $connection: Connection = {step: 'Idle', loading: true, wallets: []};
130
+ const _store = writable<Connection>($connection);
131
+ function set(connection: Connection) {
132
+ $connection = connection;
133
+ _store.set($connection);
134
+ return $connection;
135
+ }
136
+ function setError(error: {message: string; cause?: any}) {
137
+ if ($connection) {
138
+ set({
139
+ ...$connection,
140
+ error,
141
+ });
142
+ } else {
143
+ throw new Error(`no connection`);
144
+ }
145
+ }
146
+
147
+ let _wallet: {provider: EIP1193WindowWalletProvider; chainId: string} | undefined;
148
+
149
+ let popup: PopupPromise<OriginAccount> | undefined;
150
+
151
+ function fetchWallets() {
152
+ if (typeof window !== 'undefined') {
153
+ // const defaultProvider = (window as any).ethereum;
154
+ // console.log(defaultProvider);
155
+ // TODO ?
156
+ (window as any).addEventListener('eip6963:announceProvider', (event: EIP6963AnnounceProviderEvent) => {
157
+ const {detail} = event;
158
+ // const { info, provider } = detail;
159
+ // const { uuid, name, icon, rdns } = info;
160
+ // console.log('provider', provider);
161
+ // console.log(`isDefault: ${provider === defaultProvider}`);
162
+ // console.log('info', info);
163
+ const existingWallets = $connection.wallets;
164
+ existingWallets.push(detail);
165
+
166
+ set({
167
+ ...$connection,
168
+ wallets: existingWallets,
169
+ });
170
+ });
171
+ window.dispatchEvent(new Event('eip6963:requestProvider'));
172
+ }
173
+ }
174
+
175
+ function waitForWallet(name: string): Promise<EIP6963ProviderDetail> {
176
+ return new Promise((resolve, reject) => {
177
+ const timeout = setTimeout(() => {
178
+ clearInterval(interval);
179
+ reject('timeout');
180
+ }, 1000);
181
+ const interval = setInterval(() => {
182
+ const wallet = $connection.wallets.find((v) => v.info.name == name);
183
+ if (wallet) {
184
+ clearTimeout(timeout);
185
+ clearInterval(interval);
186
+ resolve(wallet);
187
+ }
188
+ }, 100);
189
+ });
190
+ }
191
+
192
+ if (autoConnect) {
193
+ if (typeof window !== 'undefined') {
194
+ // set({step: 'Idle', loading: true, wallets: $connection.wallets});
195
+ try {
196
+ const existingAccount = getOriginAccount();
197
+ if (existingAccount) {
198
+ if (existingAccount.signer) {
199
+ if (existingAccount.mechanismUsed.type == 'wallet') {
200
+ const walletMechanism = existingAccount.mechanismUsed as WalletMechanism<string, `0x${string}`>;
201
+ waitForWallet(walletMechanism.name)
202
+ .then(async (walletDetails: EIP6963ProviderDetail) => {
203
+ const walletProvider = walletDetails.provider;
204
+ const chainIdAsHex = await walletProvider.request({method: 'eth_chainId'});
205
+ const chainId = Number(chainIdAsHex).toString();
206
+ _wallet = {provider: walletProvider, chainId};
207
+ alwaysOnProvider.setWalletProvider(walletProvider);
208
+ watchForChainIdChange(_wallet.provider);
209
+ set({
210
+ step: 'SignedIn',
211
+ account: existingAccount,
212
+ mechanism: existingAccount.mechanismUsed as FullfilledMechanism,
213
+ wallets: $connection.wallets,
214
+ wallet: {
215
+ provider: walletProvider,
216
+ accountChanged: undefined,
217
+ chainId,
218
+ invalidChainId: alwaysOnChainId != chainId,
219
+ switchingChain: false,
220
+ },
221
+ });
222
+ walletProvider.request({method: 'eth_accounts'}).then(onAccountChanged);
223
+ watchForAccountChange(walletProvider);
224
+ })
225
+ .catch((err) => {
226
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
227
+ });
228
+ } else {
229
+ set({
230
+ step: 'SignedIn',
231
+ account: existingAccount,
232
+ mechanism: existingAccount.mechanismUsed as FullfilledMechanism,
233
+ wallets: $connection.wallets,
234
+ wallet: undefined,
235
+ });
236
+ }
237
+ } else {
238
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
239
+ }
240
+ } else {
241
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
242
+ }
243
+ } catch {
244
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
245
+ }
246
+ }
247
+ } else {
248
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
249
+ }
250
+ fetchWallets();
251
+
252
+ function getOriginAccount(): OriginAccount | undefined {
253
+ const fromStorage = localStorage.getItem(storageAccountKey);
254
+ if (fromStorage) {
255
+ return JSON.parse(fromStorage) as OriginAccount;
256
+ }
257
+ }
258
+ function saveOriginAccount(account: OriginAccount) {
259
+ const accountSTR = JSON.stringify(account);
260
+ sessionStorage.setItem(storageAccountKey, accountSTR);
261
+ localStorage.setItem(storageAccountKey, accountSTR);
262
+ }
263
+ function deleteOriginAccount() {
264
+ sessionStorage.removeItem(storageAccountKey);
265
+ localStorage.removeItem(storageAccountKey);
266
+ }
267
+
268
+ async function requestSignature() {
269
+ if ($connection.step !== 'NeedWalletSignature') {
270
+ throw new Error(`invalid step: ${$connection.step}, needs to be NeedWalletSignature`);
271
+ }
272
+
273
+ if (!_wallet) {
274
+ // TODO error ?
275
+ throw new Error(`no wallet provided initialised`);
276
+ }
277
+ const provider = _wallet.provider;
278
+ const chainId = _wallet.chainId;
279
+ const message = originKeyMessage(origin);
280
+ const msg = hashMessage(message);
281
+
282
+ set({
283
+ ...$connection,
284
+ step: 'WaitingForSignature',
285
+ });
286
+
287
+ let signature: `0x${string}`;
288
+ try {
289
+ // TODO timeout
290
+ signature = await provider.request({
291
+ method: 'personal_sign',
292
+ params: [msg, $connection.mechanism.address],
293
+ });
294
+ } catch (err) {
295
+ // TODO handle rejection (code: 4001 ?)
296
+ set({
297
+ ...$connection,
298
+ step: 'NeedWalletSignature',
299
+ mechanism: {
300
+ type: 'wallet',
301
+ name: $connection.mechanism.name,
302
+ address: $connection.mechanism.address,
303
+ },
304
+ error: {message: 'failed to sign message', cause: err},
305
+ });
306
+ return;
307
+ }
308
+
309
+ const originKey = fromSignatureToKey(signature);
310
+ const originMnemonic = fromEntropyKeyToMnemonic(originKey);
311
+ const originAccount = fromMnemonicToFirstAccount(originMnemonic);
312
+
313
+ const account = {
314
+ address: $connection.mechanism.address as `0x${string}`,
315
+ signer: {
316
+ origin,
317
+ address: originAccount.address,
318
+ publicKey: originAccount.publicKey,
319
+ privateKey: originAccount.privateKey,
320
+ mnemonicKey: originKey,
321
+ },
322
+ metadata: {},
323
+ mechanismUsed: $connection.mechanism,
324
+ savedPublicKeyPublicationSignature: undefined,
325
+ };
326
+ set({
327
+ ...$connection,
328
+ step: 'SignedIn',
329
+ mechanism: {
330
+ type: 'wallet',
331
+ name: $connection.mechanism.name,
332
+ address: $connection.mechanism.address,
333
+ },
334
+ account,
335
+ wallet: {
336
+ chainId,
337
+ provider: provider,
338
+ accountChanged: undefined, // TODO check account list
339
+ invalidChainId: alwaysOnChainId != chainId,
340
+ switchingChain: false,
341
+ },
342
+ });
343
+ if (remember) {
344
+ saveOriginAccount(account);
345
+ }
346
+ }
347
+
348
+ function connectOnCurrentWalletAccount(address: `0x${string}`) {
349
+ if ($connection.step === 'SignedIn' && $connection.mechanism.type === 'wallet') {
350
+ connect({
351
+ type: 'wallet',
352
+ address,
353
+ name: $connection.mechanism.name,
354
+ });
355
+ } else {
356
+ throw new Error(`need to be using a mechanism of type wallet and be SignedIN`);
357
+ }
358
+ }
359
+
360
+ function onChainChanged(chainIdAsHex: `0x${string}`) {
361
+ const chainId = Number(chainIdAsHex).toString();
362
+ if (_wallet) {
363
+ _wallet.chainId = chainId;
364
+ }
365
+ if ($connection.step === 'SignedIn' && $connection.wallet && $connection.wallet.chainId != chainId) {
366
+ set({
367
+ ...$connection,
368
+ wallet: {
369
+ ...$connection.wallet,
370
+ chainId,
371
+ invalidChainId: alwaysOnChainId != chainId,
372
+ },
373
+ });
374
+ }
375
+ }
376
+
377
+ function onAccountChanged(accounts: `0x${string}`[]) {
378
+ const accountsFormated = accounts.map((a) => a.toLowerCase()) as `0x${string}`[];
379
+ if ($connection.step === 'SignedIn' && $connection.mechanism.type === 'wallet') {
380
+ // TODO if auto-connect and saved-signature ?
381
+ // connect(
382
+ // {
383
+ // type: 'wallet',
384
+ // address: accounts[0],
385
+ // name: $connection.mechanism.name
386
+ // },
387
+ // { requireUserConfirmationBeforeSIgnatureRequest: true }
388
+ // );
389
+
390
+ if ($connection.wallet && accountsFormated.length > 0 && accountsFormated[0] != $connection.account.address) {
391
+ set({
392
+ ...$connection,
393
+ wallet: {
394
+ ...$connection.wallet,
395
+ accountChanged: accountsFormated[0],
396
+ },
397
+ });
398
+ } else if ($connection.wallet) {
399
+ set({
400
+ ...$connection,
401
+ wallet: {
402
+ ...$connection.wallet,
403
+ accountChanged: undefined,
404
+ },
405
+ });
406
+ }
407
+ }
408
+
409
+ // if (accounts[0] !== $connection)
410
+ }
411
+
412
+ function watchForAccountChange(walletProvider: EIP1193WindowWalletProvider) {
413
+ walletProvider.on('accountsChanged', onAccountChanged);
414
+ }
415
+ function stopatchingForAccountChange(walletProvider: EIP1193WindowWalletProvider) {
416
+ walletProvider.removeListener('accountsChanged', onAccountChanged);
417
+ }
418
+
419
+ function watchForChainIdChange(walletProvider: EIP1193WindowWalletProvider) {
420
+ walletProvider.on('chainChanged', onChainChanged);
421
+ }
422
+ function stopatchingForChainIdChange(walletProvider: EIP1193WindowWalletProvider) {
423
+ walletProvider.removeListener('chainChanged', onChainChanged);
424
+ }
425
+
426
+ let remember: boolean = false;
427
+ async function connect(
428
+ mechanism?: Mechanism,
429
+ options?: {
430
+ requireUserConfirmationBeforeSIgnatureRequest?: boolean;
431
+ doNotStoreLocally?: boolean;
432
+ requestSignatureRightAway?: boolean;
433
+ },
434
+ ) {
435
+ remember = !(options?.doNotStoreLocally || false);
436
+ if (mechanism) {
437
+ if (mechanism.type === 'wallet') {
438
+ const walletName = mechanism.name;
439
+ if (walletName) {
440
+ const wallet = $connection.wallets.find((v) => v.info.name == walletName || v.info.uuid == walletName);
441
+ if (wallet) {
442
+ if (_wallet) {
443
+ alwaysOnProvider.setWalletProvider(undefined);
444
+ stopatchingForAccountChange(_wallet.provider);
445
+ stopatchingForChainIdChange(_wallet.provider);
446
+ }
447
+
448
+ const mechanism: WalletMechanism<string, undefined> = {
449
+ type: 'wallet',
450
+ name: walletName,
451
+ };
452
+
453
+ set({
454
+ step: 'WaitingForWalletConnection', // TODO FetchingAccounts
455
+ mechanism,
456
+ wallets: $connection.wallets,
457
+ });
458
+ const provider = wallet.provider;
459
+ const chainIdAsHex = await provider.request({method: 'eth_chainId'});
460
+ const chainId = Number(chainIdAsHex).toString();
461
+ _wallet = {
462
+ chainId,
463
+ provider,
464
+ };
465
+ alwaysOnProvider.setWalletProvider(_wallet.provider);
466
+ watchForChainIdChange(_wallet.provider);
467
+ let accounts = await provider.request({method: 'eth_accounts'});
468
+ accounts = accounts.map((v) => v.toLowerCase()) as `0x${string}`[];
469
+ if (accounts.length === 0) {
470
+ set({
471
+ step: 'WaitingForWalletConnection',
472
+ mechanism,
473
+ wallets: $connection.wallets,
474
+ });
475
+ accounts = await provider.request({method: 'eth_requestAccounts'});
476
+ accounts = accounts.map((v) => v.toLowerCase()) as `0x${string}`[];
477
+ if (accounts.length > 0) {
478
+ if (options?.requestSignatureRightAway) {
479
+ watchForAccountChange(_wallet.provider);
480
+ set({
481
+ step: 'NeedWalletSignature',
482
+ mechanism: {
483
+ ...mechanism,
484
+ address: accounts[0],
485
+ },
486
+ wallets: $connection.wallets,
487
+ });
488
+ await requestSignature();
489
+ } else {
490
+ set({
491
+ step: 'NeedWalletSignature',
492
+ mechanism: {
493
+ ...mechanism,
494
+ address: accounts[0],
495
+ },
496
+ wallets: $connection.wallets,
497
+ });
498
+ watchForAccountChange(_wallet.provider);
499
+ }
500
+ } else {
501
+ set({
502
+ step: 'MechanismToChoose',
503
+ wallets: $connection.wallets,
504
+ error: {message: 'could not get any accounts'},
505
+ });
506
+ }
507
+ } else {
508
+ if (options?.requireUserConfirmationBeforeSIgnatureRequest) {
509
+ set({
510
+ step: 'NeedWalletSignature',
511
+ mechanism: {
512
+ ...mechanism,
513
+ address: accounts[0],
514
+ },
515
+ wallets: $connection.wallets,
516
+ });
517
+ watchForAccountChange(_wallet.provider);
518
+ } else {
519
+ watchForAccountChange(_wallet.provider);
520
+ set({
521
+ step: 'NeedWalletSignature',
522
+ mechanism: {
523
+ ...mechanism,
524
+ address: accounts[0],
525
+ },
526
+ wallets: $connection.wallets,
527
+ });
528
+ await requestSignature();
529
+ }
530
+ }
531
+ } else {
532
+ console.error(`failed to get wallet ${walletName}`, $connection.wallets);
533
+ set({
534
+ step: 'MechanismToChoose',
535
+ wallets: $connection.wallets,
536
+ error: {message: `failed to get wallet ${walletName}`},
537
+ });
538
+ }
539
+ } else {
540
+ // TODO can also be done automatically before hand
541
+ // set({
542
+ // step: 'FetchingWallets',
543
+ // mechanism: { type: 'wallet', wallet: undefined }
544
+ // });
545
+
546
+ set({
547
+ step: 'WalletToChoose',
548
+ mechanism: {type: 'wallet'},
549
+ wallets: $connection.wallets,
550
+ });
551
+ }
552
+ } else {
553
+ popup = connectViaPopup({
554
+ mechanism,
555
+ walletHost: settings.walletHost,
556
+ });
557
+ set({
558
+ step: 'PopupLaunched',
559
+ popupClosed: false,
560
+ mechanism,
561
+ wallets: $connection.wallets,
562
+ });
563
+
564
+ const unsubscribe = popup.subscribe(($popup) => {
565
+ if ($connection?.step === 'PopupLaunched') {
566
+ if ($popup.closed) {
567
+ set({
568
+ ...$connection,
569
+ popupClosed: true,
570
+ });
571
+ }
572
+ }
573
+ });
574
+ try {
575
+ const result = await popup;
576
+ console.log({result});
577
+ set({
578
+ step: 'SignedIn',
579
+ account: result,
580
+ mechanism,
581
+ wallets: $connection.wallets,
582
+ wallet: undefined,
583
+ });
584
+ if (remember) {
585
+ saveOriginAccount(result);
586
+ }
587
+ } catch (err) {
588
+ console.log({error: err});
589
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
590
+ } finally {
591
+ unsubscribe();
592
+ }
593
+ }
594
+ } else {
595
+ set({
596
+ step: 'MechanismToChoose',
597
+ wallets: $connection.wallets,
598
+ });
599
+ }
600
+ }
601
+
602
+ function disconnect() {
603
+ deleteOriginAccount();
604
+ if (_wallet) {
605
+ alwaysOnProvider.setWalletProvider(undefined);
606
+ stopatchingForAccountChange(_wallet.provider);
607
+ stopatchingForChainIdChange(_wallet.provider);
608
+ }
609
+ _wallet = undefined;
610
+ set({
611
+ step: 'Idle',
612
+ loading: false,
613
+ wallets: $connection.wallets,
614
+ });
615
+ }
616
+
617
+ function back(step: 'MechanismToChoose' | 'Idle' | 'WalletToChoose') {
618
+ popup?.cancel();
619
+ if (step === 'MechanismToChoose') {
620
+ set({step, wallets: $connection.wallets});
621
+ } else if (step === 'Idle') {
622
+ set({step, loading: false, wallets: $connection.wallets});
623
+ } else if (step === 'WalletToChoose') {
624
+ set({step, wallets: $connection.wallets, mechanism: {type: 'wallet'}});
625
+ }
626
+ }
627
+
628
+ const popupLauncher = createPopupLauncher<OriginAccount>();
629
+
630
+ function connectViaPopup(settings: PopupSettings) {
631
+ let popupURL = new URL(`${settings.walletHost}/login/`);
632
+ let fullWindow = false;
633
+ if (settings.mechanism.type === 'mnemonic') {
634
+ popupURL.searchParams.append('type', 'mnemonic');
635
+ } else if (settings.mechanism.type === 'email') {
636
+ popupURL.searchParams.append('type', 'email');
637
+ if (settings.mechanism.email) {
638
+ popupURL.searchParams.append('email', encodeURIComponent(settings.mechanism.email));
639
+ }
640
+ if (settings.mechanism.mode) {
641
+ popupURL.searchParams.append('emailMode', settings.mechanism.mode);
642
+ }
643
+ } else if (settings.mechanism.type === 'oauth') {
644
+ popupURL.searchParams.append('type', 'oauth');
645
+
646
+ if (settings.mechanism.provider.id === 'auth0') {
647
+ popupURL.searchParams.append('oauth-provider', settings.mechanism.provider.id);
648
+ popupURL.searchParams.append('oauth-connection', settings.mechanism.provider.connection);
649
+ } else {
650
+ popupURL.searchParams.append('oauth-provider', settings.mechanism.provider.id);
651
+ }
652
+
653
+ if (!settings.mechanism.usePopup) {
654
+ popupURL.searchParams.append('oauth-redirection', 'true');
655
+ }
656
+ } else {
657
+ throw new Error(`mechanism ${(settings.mechanism as any).type} not supported`);
658
+ }
659
+
660
+ // if (settings.extraParams) {
661
+ // for (const [key, value] of Object.entries(settings.extraParams)) {
662
+ // popupURL.searchParams.append(`${key}`, value);
663
+ // }
664
+ // }
665
+
666
+ const currentURL = new URL(location.href);
667
+
668
+ const entriesToAdd: [string, string][] = [];
669
+ currentURL.searchParams.forEach((value, key) => {
670
+ if (key.startsWith('renraku_')) {
671
+ entriesToAdd.push([key.slice(`renraku_`.length), value]);
672
+ }
673
+ });
674
+
675
+ if (currentURL.searchParams.has('eruda')) {
676
+ entriesToAdd.push(['eruda', currentURL.searchParams.get('eruda') || '']);
677
+ }
678
+ if (currentURL.searchParams.has('eruda')) {
679
+ entriesToAdd.push(['eruda', currentURL.searchParams.get('eruda') || '']);
680
+ }
681
+
682
+ for (const entryToAdd of entriesToAdd) {
683
+ popupURL.searchParams.append(entryToAdd[0], entryToAdd[1]);
684
+ }
685
+ return popupLauncher.launchPopup(popupURL.toString(), {fullWindow});
686
+ }
687
+
688
+ function cancel() {
689
+ popup?.cancel();
690
+ set({step: 'Idle', loading: false, wallets: $connection.wallets});
691
+ }
692
+
693
+ function getSignatureForPublicKeyPublication(): Promise<`0x${string}`> {
694
+ if ($connection.step !== 'SignedIn') {
695
+ throw new Error('Not signed in');
696
+ }
697
+ const account = $connection.account;
698
+ if ($connection.mechanism.type === 'wallet') {
699
+ if (!_wallet) {
700
+ throw new Error(`no provider`);
701
+ }
702
+ const message = originPublicKeyPublicationMessage(origin, account.signer.publicKey);
703
+ const msg = hashMessage(message);
704
+ return _wallet.provider.request({
705
+ method: 'personal_sign',
706
+ params: [msg, account.address],
707
+ });
708
+ }
709
+
710
+ if (account.savedPublicKeyPublicationSignature) {
711
+ return Promise.resolve(account.savedPublicKeyPublicationSignature);
712
+ }
713
+
714
+ // TODO offer a way to use iframe + popup to sign the message
715
+ // this would require saving mnemonic or privatekey on etherplay localstorage though
716
+ throw new Error(`no saved public key publication signature for ${account.address}`);
717
+ }
718
+
719
+ async function switchWalletChain(
720
+ chainId: string,
721
+ config?: {
722
+ readonly rpcUrls?: readonly string[];
723
+ readonly blockExplorerUrls?: readonly string[];
724
+ readonly chainName?: string;
725
+ readonly iconUrls?: readonly string[];
726
+ readonly nativeCurrency?: {
727
+ name: string;
728
+ symbol: string;
729
+ decimals: number;
730
+ };
731
+ },
732
+ ) {
733
+ if ($connection.step !== 'SignedIn' || !$connection.wallet) {
734
+ throw new Error(`invali state`);
735
+ }
736
+
737
+ const wallet = $connection.wallet;
738
+ // if (!wallet) {
739
+ // throw new Error(`no wallet`);
740
+ // }
741
+
742
+ try {
743
+ // attempt to switch...
744
+ set({
745
+ ...$connection,
746
+ wallet: {...$connection.wallet, switchingChain: 'switchingChain'},
747
+ });
748
+ const result = await wallet.provider.request({
749
+ method: 'wallet_switchEthereumChain',
750
+ params: [
751
+ {
752
+ chainId: ('0x' + parseInt(chainId).toString(16)) as EIP1193ChainId,
753
+ },
754
+ ],
755
+ });
756
+ if (!result) {
757
+ if ($connection.wallet) {
758
+ set({
759
+ ...$connection,
760
+ wallet: {...$connection.wallet, switchingChain: false},
761
+ });
762
+ }
763
+
764
+ // logger.info(`wallet_switchEthereumChain: complete`);
765
+ // this will be taken care with `chainChanged` (but maybe it should be done there ?)
766
+ // handleNetwork(chainId);
767
+ } else {
768
+ if ($connection.wallet) {
769
+ set({
770
+ ...$connection,
771
+ wallet: {...$connection.wallet, switchingChain: false},
772
+ error: {
773
+ message: `Failed to switch to ${config?.chainName || `chain with id = ${chainId}`}`,
774
+ cause: result,
775
+ },
776
+ });
777
+ }
778
+ throw result;
779
+ }
780
+ } catch (err) {
781
+ if ((err as any).code === 4001) {
782
+ // logger.info(`wallet_addEthereumChain: failed but error code === 4001, we ignore as user rejected it`, err);
783
+ if ($connection.wallet) {
784
+ set({
785
+ ...$connection,
786
+ wallet: {...$connection.wallet, switchingChain: false},
787
+ });
788
+ }
789
+ return;
790
+ }
791
+ // if ((err as any).code === 4902) {
792
+ else if (config && config.rpcUrls && config.rpcUrls.length > 0) {
793
+ if ($connection.wallet) {
794
+ set({
795
+ ...$connection,
796
+ wallet: {...$connection.wallet, switchingChain: 'addingChain'},
797
+ });
798
+ }
799
+ // logger.info(`wallet_switchEthereumChain: could not switch, try adding the chain via "wallet_addEthereumChain"`);
800
+ try {
801
+ const result = await wallet.provider.request({
802
+ method: 'wallet_addEthereumChain',
803
+ params: [
804
+ {
805
+ chainId: ('0x' + parseInt(chainId).toString(16)) as EIP1193ChainId,
806
+ rpcUrls: config.rpcUrls,
807
+ chainName: config.chainName,
808
+ blockExplorerUrls: config.blockExplorerUrls,
809
+ iconUrls: config.iconUrls,
810
+ nativeCurrency: config.nativeCurrency,
811
+ },
812
+ ],
813
+ });
814
+ if (!result) {
815
+ if ($connection.wallet) {
816
+ set({
817
+ ...$connection,
818
+ wallet: {...$connection.wallet, switchingChain: false},
819
+ });
820
+ }
821
+ // this will be taken care with `chainChanged` (but maybe it should be done there ?)
822
+ // handleNetwork(chainId);
823
+ } else {
824
+ if ($connection.wallet) {
825
+ set({
826
+ ...$connection,
827
+ wallet: {...$connection.wallet, switchingChain: false},
828
+ error: {
829
+ message: `Failed to add new chain: ${config?.chainName || `chain with id = ${chainId}`}`,
830
+ cause: result,
831
+ },
832
+ });
833
+ }
834
+ // logger.info(`wallet_addEthereumChain: a non-undefinded result means an error`, result);
835
+ throw result;
836
+ }
837
+ } catch (err) {
838
+ if ((err as any).code !== 4001) {
839
+ if ($connection.wallet) {
840
+ set({
841
+ ...$connection,
842
+ wallet: {...$connection.wallet, switchingChain: false},
843
+ error: {
844
+ message: `Failed to add new chain: ${config?.chainName || `chain with id = ${chainId}`}`,
845
+ cause: err,
846
+ },
847
+ });
848
+ }
849
+ // logger.info(`wallet_addEthereumChain: failed`, err);
850
+ // TODO ?
851
+ // set({
852
+ // error: {message: `Failed to add new chain`, cause: err},
853
+ // });
854
+ // for now:
855
+ throw err;
856
+ } else {
857
+ if ($connection.wallet) {
858
+ set({
859
+ ...$connection,
860
+ wallet: {...$connection.wallet, switchingChain: false},
861
+ });
862
+ }
863
+ // logger.info(`wallet_addEthereumChain: failed but error code === 4001, we ignore as user rejected it`, err);
864
+ return;
865
+ }
866
+ }
867
+ } else {
868
+ const errorMessage = `Chain "${config?.chainName || `with chainId = ${chainId}`} " is not available on your wallet`;
869
+ if ($connection.wallet) {
870
+ set({
871
+ ...$connection,
872
+ wallet: {...$connection.wallet, switchingChain: false},
873
+ error: {
874
+ message: errorMessage,
875
+ },
876
+ });
877
+ }
878
+
879
+ throw new Error(errorMessage);
880
+ }
881
+ }
882
+ }
883
+
884
+ return {
885
+ subscribe: _store.subscribe,
886
+ connect,
887
+ cancel,
888
+ back,
889
+ requestSignature,
890
+ connectOnCurrentWalletAccount,
891
+ disconnect,
892
+ getSignatureForPublicKeyPublication,
893
+ switchWalletChain,
894
+ provider: alwaysOnProvider,
895
+ };
896
+ }
897
+
898
+ export type ConnectionStore = ReturnType<typeof createConnection>;