@etherplay/connect 0.0.37 → 0.0.39

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.
@@ -0,0 +1,954 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createConnection, isTargetStepReached } from './index.js';
3
+ // Mock the @etherplay/alchemy module
4
+ vi.mock('@etherplay/alchemy', () => ({
5
+ fromEntropyKeyToMnemonic: vi.fn((key) => 'test mnemonic words here'),
6
+ fromSignatureToKey: vi.fn((signature) => '0x1234567890abcdef'),
7
+ originKeyMessage: vi.fn((origin) => `Sign this message to prove ownership: ${origin}`),
8
+ originPublicKeyPublicationMessage: vi.fn((origin, publicKey) => `Publish public key: ${publicKey} for ${origin}`),
9
+ }));
10
+ // Create a mock wallet provider
11
+ function createMockWalletProvider(accounts = [], chainId = '0x1') {
12
+ const accountsChangedListeners = new Set();
13
+ const chainChangedListeners = new Set();
14
+ const underlyingProvider = {
15
+ request: vi.fn(),
16
+ };
17
+ return {
18
+ underlyingProvider,
19
+ signMessage: vi.fn().mockResolvedValue('0xsignature1234'),
20
+ getChainId: vi.fn().mockResolvedValue(chainId),
21
+ requestAccounts: vi.fn().mockResolvedValue(accounts),
22
+ getAccounts: vi.fn().mockResolvedValue(accounts),
23
+ listenForAccountsChanged: vi.fn((handler) => {
24
+ accountsChangedListeners.add(handler);
25
+ }),
26
+ stopListenForAccountsChanged: vi.fn((handler) => {
27
+ accountsChangedListeners.delete(handler);
28
+ }),
29
+ listenForChainChanged: vi.fn((handler) => {
30
+ chainChangedListeners.add(handler);
31
+ }),
32
+ stopListenForChainChanged: vi.fn((handler) => {
33
+ chainChangedListeners.delete(handler);
34
+ }),
35
+ switchChain: vi.fn().mockResolvedValue(null),
36
+ addChain: vi.fn().mockResolvedValue(null),
37
+ };
38
+ }
39
+ // Create a mock wallet handle
40
+ function createMockWalletHandle(name = 'MockWallet', accounts = ['0xabc123'], chainId = '0x1') {
41
+ return {
42
+ info: {
43
+ uuid: `uuid-${name}`,
44
+ name,
45
+ icon: 'data:image/svg+xml,...',
46
+ rdns: `com.mock.${name.toLowerCase()}`,
47
+ },
48
+ walletProvider: createMockWalletProvider(accounts, chainId),
49
+ };
50
+ }
51
+ // Create a mock account generator
52
+ function createMockAccountGenerator() {
53
+ return {
54
+ type: 'secp256k1',
55
+ fromMnemonicToAccount: vi.fn((mnemonic, index) => ({
56
+ address: '0xoriginaddress',
57
+ publicKey: '0xpublickey',
58
+ privateKey: '0xprivatekey',
59
+ })),
60
+ signTextMessage: vi.fn().mockResolvedValue('0xsig'),
61
+ };
62
+ }
63
+ // Create a mock wallet connector
64
+ function createMockWalletConnector(walletHandles = []) {
65
+ const accountGenerator = createMockAccountGenerator();
66
+ const alwaysOnProvider = {
67
+ chainId: '1',
68
+ provider: { request: vi.fn() },
69
+ setWalletProvider: vi.fn(),
70
+ setWalletStatus: vi.fn(),
71
+ };
72
+ return {
73
+ fetchWallets: vi.fn((callback) => {
74
+ // Simulate async wallet announcement
75
+ walletHandles.forEach((handle, index) => {
76
+ setTimeout(() => callback(handle), index * 10);
77
+ });
78
+ }),
79
+ createAlwaysOnProvider: vi.fn(() => alwaysOnProvider),
80
+ accountGenerator,
81
+ };
82
+ }
83
+ // Default chain info for tests
84
+ const defaultChainInfo = {
85
+ id: 1,
86
+ name: 'Ethereum Mainnet',
87
+ rpcUrls: {
88
+ default: {
89
+ http: ['https://eth-mainnet.example.com'],
90
+ },
91
+ },
92
+ nativeCurrency: {
93
+ name: 'Ether',
94
+ symbol: 'ETH',
95
+ decimals: 18,
96
+ },
97
+ };
98
+ describe('isTargetStepReached', () => {
99
+ describe('with targetStep: SignedIn', () => {
100
+ it('should return true when step is SignedIn with popup-based auth', () => {
101
+ const connection = {
102
+ step: 'SignedIn',
103
+ mechanism: { type: 'email', email: 'test@example.com', mode: 'otp' },
104
+ account: {
105
+ address: '0x123',
106
+ signer: {
107
+ origin: 'test',
108
+ address: '0xorigin',
109
+ publicKey: '0xpub',
110
+ privateKey: '0xpriv',
111
+ mnemonicKey: '0xmnem',
112
+ },
113
+ metadata: {},
114
+ mechanismUsed: { type: 'email', email: 'test@example.com', mode: 'otp' },
115
+ savedPublicKeyPublicationSignature: undefined,
116
+ accountType: 'secp256k1',
117
+ },
118
+ wallet: undefined,
119
+ wallets: [],
120
+ };
121
+ expect(isTargetStepReached(connection, 'SignedIn')).toBe(true);
122
+ });
123
+ it('should return true when step is SignedIn with wallet-based auth', () => {
124
+ const mockWalletProvider = createMockWalletProvider(['0xabc']);
125
+ const walletMechanism = { type: 'wallet', name: 'MockWallet', address: '0xabc' };
126
+ const connection = {
127
+ step: 'SignedIn',
128
+ mechanism: walletMechanism,
129
+ account: {
130
+ address: '0xabc',
131
+ signer: {
132
+ origin: 'test',
133
+ address: '0xorigin',
134
+ publicKey: '0xpub',
135
+ privateKey: '0xpriv',
136
+ mnemonicKey: '0xmnem',
137
+ },
138
+ metadata: {},
139
+ mechanismUsed: walletMechanism,
140
+ savedPublicKeyPublicationSignature: undefined,
141
+ accountType: 'secp256k1',
142
+ },
143
+ wallet: {
144
+ provider: mockWalletProvider,
145
+ accounts: ['0xabc'],
146
+ status: 'connected',
147
+ chainId: '1',
148
+ invalidChainId: false,
149
+ switchingChain: false,
150
+ },
151
+ wallets: [],
152
+ };
153
+ expect(isTargetStepReached(connection, 'SignedIn')).toBe(true);
154
+ });
155
+ it('should return false when step is not SignedIn', () => {
156
+ const connection = {
157
+ step: 'Idle',
158
+ loading: false,
159
+ wallet: undefined,
160
+ wallets: [],
161
+ };
162
+ expect(isTargetStepReached(connection, 'SignedIn')).toBe(false);
163
+ });
164
+ it('should return false when step is WalletConnected', () => {
165
+ const mockWalletProvider = createMockWalletProvider(['0xabc']);
166
+ const connection = {
167
+ step: 'WalletConnected',
168
+ mechanism: { type: 'wallet', name: 'MockWallet', address: '0xabc' },
169
+ account: { address: '0xabc' },
170
+ wallet: {
171
+ provider: mockWalletProvider,
172
+ accounts: ['0xabc'],
173
+ status: 'connected',
174
+ chainId: '1',
175
+ invalidChainId: false,
176
+ switchingChain: false,
177
+ },
178
+ wallets: [],
179
+ };
180
+ expect(isTargetStepReached(connection, 'SignedIn')).toBe(false);
181
+ });
182
+ });
183
+ describe('with targetStep: WalletConnected', () => {
184
+ it('should return true when step is WalletConnected', () => {
185
+ const mockWalletProvider = createMockWalletProvider(['0xabc']);
186
+ const connection = {
187
+ step: 'WalletConnected',
188
+ mechanism: { type: 'wallet', name: 'MockWallet', address: '0xabc' },
189
+ account: { address: '0xabc' },
190
+ wallet: {
191
+ provider: mockWalletProvider,
192
+ accounts: ['0xabc'],
193
+ status: 'connected',
194
+ chainId: '1',
195
+ invalidChainId: false,
196
+ switchingChain: false,
197
+ },
198
+ wallets: [],
199
+ };
200
+ expect(isTargetStepReached(connection, 'WalletConnected')).toBe(true);
201
+ });
202
+ it('should return true when step is SignedIn with wallet', () => {
203
+ const mockWalletProvider = createMockWalletProvider(['0xabc']);
204
+ const walletMechanism2 = { type: 'wallet', name: 'MockWallet', address: '0xabc' };
205
+ const connection = {
206
+ step: 'SignedIn',
207
+ mechanism: walletMechanism2,
208
+ account: {
209
+ address: '0xabc',
210
+ signer: {
211
+ origin: 'test',
212
+ address: '0xorigin',
213
+ publicKey: '0xpub',
214
+ privateKey: '0xpriv',
215
+ mnemonicKey: '0xmnem',
216
+ },
217
+ metadata: {},
218
+ mechanismUsed: walletMechanism2,
219
+ savedPublicKeyPublicationSignature: undefined,
220
+ accountType: 'secp256k1',
221
+ },
222
+ wallet: {
223
+ provider: mockWalletProvider,
224
+ accounts: ['0xabc'],
225
+ status: 'connected',
226
+ chainId: '1',
227
+ invalidChainId: false,
228
+ switchingChain: false,
229
+ },
230
+ wallets: [],
231
+ };
232
+ expect(isTargetStepReached(connection, 'WalletConnected')).toBe(true);
233
+ });
234
+ it('should return false when step is SignedIn without wallet', () => {
235
+ const connection = {
236
+ step: 'SignedIn',
237
+ mechanism: { type: 'email', email: 'test@example.com', mode: 'otp' },
238
+ account: {
239
+ address: '0x123',
240
+ signer: {
241
+ origin: 'test',
242
+ address: '0xorigin',
243
+ publicKey: '0xpub',
244
+ privateKey: '0xpriv',
245
+ mnemonicKey: '0xmnem',
246
+ },
247
+ metadata: {},
248
+ mechanismUsed: { type: 'email', email: 'test@example.com', mode: 'otp' },
249
+ savedPublicKeyPublicationSignature: undefined,
250
+ accountType: 'secp256k1',
251
+ },
252
+ wallet: undefined,
253
+ wallets: [],
254
+ };
255
+ expect(isTargetStepReached(connection, 'WalletConnected')).toBe(false);
256
+ });
257
+ it('should return false when step is Idle', () => {
258
+ const connection = {
259
+ step: 'Idle',
260
+ loading: false,
261
+ wallet: undefined,
262
+ wallets: [],
263
+ };
264
+ expect(isTargetStepReached(connection, 'WalletConnected')).toBe(false);
265
+ });
266
+ });
267
+ describe('with walletOnly: true', () => {
268
+ it('should return true for SignedIn with wallet when walletOnly is true', () => {
269
+ const mockWalletProvider = createMockWalletProvider(['0xabc']);
270
+ const walletMechanism3 = { type: 'wallet', name: 'MockWallet', address: '0xabc' };
271
+ const connection = {
272
+ step: 'SignedIn',
273
+ mechanism: walletMechanism3,
274
+ account: {
275
+ address: '0xabc',
276
+ signer: {
277
+ origin: 'test',
278
+ address: '0xorigin',
279
+ publicKey: '0xpub',
280
+ privateKey: '0xpriv',
281
+ mnemonicKey: '0xmnem',
282
+ },
283
+ metadata: {},
284
+ mechanismUsed: walletMechanism3,
285
+ savedPublicKeyPublicationSignature: undefined,
286
+ accountType: 'secp256k1',
287
+ },
288
+ wallet: {
289
+ provider: mockWalletProvider,
290
+ accounts: ['0xabc'],
291
+ status: 'connected',
292
+ chainId: '1',
293
+ invalidChainId: false,
294
+ switchingChain: false,
295
+ },
296
+ wallets: [],
297
+ };
298
+ expect(isTargetStepReached(connection, 'SignedIn', true)).toBe(true);
299
+ });
300
+ it('should return false for SignedIn without wallet when walletOnly is true', () => {
301
+ const connection = {
302
+ step: 'SignedIn',
303
+ mechanism: { type: 'email', email: 'test@example.com', mode: 'otp' },
304
+ account: {
305
+ address: '0x123',
306
+ signer: {
307
+ origin: 'test',
308
+ address: '0xorigin',
309
+ publicKey: '0xpub',
310
+ privateKey: '0xpriv',
311
+ mnemonicKey: '0xmnem',
312
+ },
313
+ metadata: {},
314
+ mechanismUsed: { type: 'email', email: 'test@example.com', mode: 'otp' },
315
+ savedPublicKeyPublicationSignature: undefined,
316
+ accountType: 'secp256k1',
317
+ },
318
+ wallet: undefined,
319
+ wallets: [],
320
+ };
321
+ expect(isTargetStepReached(connection, 'SignedIn', true)).toBe(false);
322
+ });
323
+ });
324
+ });
325
+ describe('createConnection', () => {
326
+ beforeEach(() => {
327
+ // Clear localStorage before each test
328
+ localStorage.clear();
329
+ sessionStorage.clear();
330
+ vi.useFakeTimers();
331
+ });
332
+ afterEach(() => {
333
+ vi.clearAllMocks();
334
+ vi.useRealTimers();
335
+ });
336
+ describe('initialization', () => {
337
+ it('should create a connection store with required properties', () => {
338
+ const walletConnector = createMockWalletConnector();
339
+ const store = createConnection({
340
+ walletHost: 'https://wallet.example.com',
341
+ chainInfo: defaultChainInfo,
342
+ walletConnector,
343
+ });
344
+ expect(store).toBeDefined();
345
+ expect(typeof store.subscribe).toBe('function');
346
+ expect(typeof store.connect).toBe('function');
347
+ expect(typeof store.cancel).toBe('function');
348
+ expect(typeof store.back).toBe('function');
349
+ expect(typeof store.disconnect).toBe('function');
350
+ expect(typeof store.requestSignature).toBe('function');
351
+ expect(typeof store.connectToAddress).toBe('function');
352
+ expect(typeof store.getSignatureForPublicKeyPublication).toBe('function');
353
+ expect(typeof store.switchWalletChain).toBe('function');
354
+ expect(typeof store.unlock).toBe('function');
355
+ expect(typeof store.ensureConnected).toBe('function');
356
+ expect(typeof store.isTargetStepReached).toBe('function');
357
+ });
358
+ it('should start in Idle step with loading true when autoConnect is true (with no saved session)', () => {
359
+ const walletConnector = createMockWalletConnector();
360
+ const store = createConnection({
361
+ walletHost: 'https://wallet.example.com',
362
+ chainInfo: defaultChainInfo,
363
+ walletConnector,
364
+ autoConnect: true,
365
+ });
366
+ let currentState;
367
+ store.subscribe((state) => {
368
+ currentState = state;
369
+ });
370
+ expect(currentState?.step).toBe('Idle');
371
+ // When no saved session exists, loading becomes false after initial check
372
+ // The loading: true state is very brief and may resolve to false immediately
373
+ expect(currentState?.step === 'Idle').toBe(true);
374
+ });
375
+ it('should start in Idle step with loading false when autoConnect is false', () => {
376
+ const walletConnector = createMockWalletConnector();
377
+ const store = createConnection({
378
+ walletHost: 'https://wallet.example.com',
379
+ chainInfo: defaultChainInfo,
380
+ walletConnector,
381
+ autoConnect: false,
382
+ });
383
+ let currentState;
384
+ store.subscribe((state) => {
385
+ currentState = state;
386
+ });
387
+ expect(currentState?.step).toBe('Idle');
388
+ expect(currentState?.step === 'Idle' && currentState.loading).toBe(false);
389
+ });
390
+ it('should expose chainId and chainInfo', () => {
391
+ const walletConnector = createMockWalletConnector();
392
+ const store = createConnection({
393
+ walletHost: 'https://wallet.example.com',
394
+ chainInfo: defaultChainInfo,
395
+ walletConnector,
396
+ });
397
+ expect(store.chainId).toBe('1');
398
+ expect(store.chainInfo).toEqual(defaultChainInfo);
399
+ });
400
+ it('should set targetStep to SignedIn by default', () => {
401
+ const walletConnector = createMockWalletConnector();
402
+ const store = createConnection({
403
+ walletHost: 'https://wallet.example.com',
404
+ chainInfo: defaultChainInfo,
405
+ walletConnector,
406
+ });
407
+ expect(store.targetStep).toBe('SignedIn');
408
+ });
409
+ it('should respect custom targetStep', () => {
410
+ const walletConnector = createMockWalletConnector();
411
+ const store = createConnection({
412
+ targetStep: 'WalletConnected',
413
+ chainInfo: defaultChainInfo,
414
+ walletConnector,
415
+ });
416
+ expect(store.targetStep).toBe('WalletConnected');
417
+ });
418
+ it('should set walletOnly based on settings', () => {
419
+ const walletConnector = createMockWalletConnector();
420
+ const store = createConnection({
421
+ walletHost: 'https://wallet.example.com',
422
+ walletOnly: true,
423
+ chainInfo: defaultChainInfo,
424
+ walletConnector,
425
+ });
426
+ expect(store.walletOnly).toBe(true);
427
+ });
428
+ });
429
+ describe('connect with wallet mechanism', () => {
430
+ it('should transition to MechanismToChoose when connect is called without mechanism', async () => {
431
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
432
+ const store = createConnection({
433
+ walletHost: 'https://wallet.example.com',
434
+ chainInfo: defaultChainInfo,
435
+ walletConnector,
436
+ autoConnect: false,
437
+ });
438
+ let currentState;
439
+ store.subscribe((state) => {
440
+ currentState = state;
441
+ });
442
+ await store.connect();
443
+ expect(currentState?.step).toBe('MechanismToChoose');
444
+ });
445
+ it('should transition to WalletToChoose when connect is called with wallet mechanism but no specific wallet', async () => {
446
+ const walletConnector = createMockWalletConnector([createMockWalletHandle('Wallet1'), createMockWalletHandle('Wallet2')]);
447
+ const store = createConnection({
448
+ walletHost: 'https://wallet.example.com',
449
+ chainInfo: defaultChainInfo,
450
+ walletConnector,
451
+ autoConnect: false,
452
+ });
453
+ // Wait for wallets to be fetched
454
+ vi.advanceTimersByTime(50);
455
+ let currentState;
456
+ store.subscribe((state) => {
457
+ currentState = state;
458
+ });
459
+ await store.connect({ type: 'wallet' });
460
+ expect(currentState?.step).toBe('WalletToChoose');
461
+ });
462
+ it('should transition to WalletConnected when single wallet connects successfully', async () => {
463
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
464
+ const walletConnector = createMockWalletConnector([mockHandle]);
465
+ const store = createConnection({
466
+ walletHost: 'https://wallet.example.com',
467
+ chainInfo: defaultChainInfo,
468
+ walletConnector,
469
+ autoConnect: false,
470
+ });
471
+ // Wait for wallet to be announced
472
+ vi.advanceTimersByTime(50);
473
+ let currentState;
474
+ store.subscribe((state) => {
475
+ currentState = state;
476
+ });
477
+ const connectPromise = store.connect({ type: 'wallet' });
478
+ await vi.advanceTimersByTimeAsync(100);
479
+ await connectPromise;
480
+ expect(currentState?.step).toBe('WalletConnected');
481
+ if (currentState?.step === 'WalletConnected') {
482
+ expect(currentState.mechanism.name).toBe('MockWallet');
483
+ expect(currentState.mechanism.address).toBe('0xuser123');
484
+ }
485
+ });
486
+ it('should transition to ChooseWalletAccount when multiple accounts available', async () => {
487
+ const mockHandle = createMockWalletHandle('MockWallet', [
488
+ '0xuser1',
489
+ '0xuser2',
490
+ ]);
491
+ const walletConnector = createMockWalletConnector([mockHandle]);
492
+ const store = createConnection({
493
+ walletHost: 'https://wallet.example.com',
494
+ chainInfo: defaultChainInfo,
495
+ walletConnector,
496
+ autoConnect: false,
497
+ alwaysUseCurrentAccount: false,
498
+ });
499
+ // Wait for wallet to be announced
500
+ vi.advanceTimersByTime(50);
501
+ let currentState;
502
+ store.subscribe((state) => {
503
+ currentState = state;
504
+ });
505
+ const connectPromise = store.connect({ type: 'wallet' });
506
+ await vi.advanceTimersByTimeAsync(100);
507
+ await connectPromise;
508
+ expect(currentState?.step).toBe('ChooseWalletAccount');
509
+ if (currentState?.step === 'ChooseWalletAccount') {
510
+ expect(currentState.wallet.accounts).toEqual(['0xuser1', '0xuser2']);
511
+ }
512
+ });
513
+ it('should skip account selection when alwaysUseCurrentAccount is true', async () => {
514
+ const mockHandle = createMockWalletHandle('MockWallet', [
515
+ '0xuser1',
516
+ '0xuser2',
517
+ ]);
518
+ const walletConnector = createMockWalletConnector([mockHandle]);
519
+ const store = createConnection({
520
+ walletHost: 'https://wallet.example.com',
521
+ chainInfo: defaultChainInfo,
522
+ walletConnector,
523
+ autoConnect: false,
524
+ alwaysUseCurrentAccount: true,
525
+ });
526
+ // Wait for wallet to be announced
527
+ vi.advanceTimersByTime(50);
528
+ let currentState;
529
+ store.subscribe((state) => {
530
+ currentState = state;
531
+ });
532
+ const connectPromise = store.connect({ type: 'wallet' });
533
+ await vi.advanceTimersByTimeAsync(100);
534
+ await connectPromise;
535
+ expect(currentState?.step).toBe('WalletConnected');
536
+ });
537
+ it('should set error when wallet connection fails', async () => {
538
+ const mockHandle = createMockWalletHandle('MockWallet', []);
539
+ // Make requestAccounts return empty array to simulate connection failure
540
+ mockHandle.walletProvider.requestAccounts.mockResolvedValue([]);
541
+ const walletConnector = createMockWalletConnector([mockHandle]);
542
+ const store = createConnection({
543
+ walletHost: 'https://wallet.example.com',
544
+ chainInfo: defaultChainInfo,
545
+ walletConnector,
546
+ autoConnect: false,
547
+ });
548
+ // Wait for wallet to be announced
549
+ vi.advanceTimersByTime(50);
550
+ let currentState;
551
+ store.subscribe((state) => {
552
+ currentState = state;
553
+ });
554
+ const connectPromise = store.connect({ type: 'wallet' });
555
+ await vi.advanceTimersByTimeAsync(100);
556
+ await connectPromise;
557
+ expect(currentState?.step).toBe('MechanismToChoose');
558
+ expect(currentState?.error?.message).toBe('could not get any accounts');
559
+ });
560
+ });
561
+ describe('disconnect', () => {
562
+ it('should transition to Idle and clear storage when disconnect is called', async () => {
563
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
564
+ const walletConnector = createMockWalletConnector([mockHandle]);
565
+ const store = createConnection({
566
+ walletHost: 'https://wallet.example.com',
567
+ chainInfo: defaultChainInfo,
568
+ walletConnector,
569
+ autoConnect: false,
570
+ });
571
+ // Wait for wallet to be announced
572
+ vi.advanceTimersByTime(50);
573
+ let currentState;
574
+ store.subscribe((state) => {
575
+ currentState = state;
576
+ });
577
+ // Connect first
578
+ const connectPromise = store.connect({ type: 'wallet' });
579
+ await vi.advanceTimersByTimeAsync(100);
580
+ await connectPromise;
581
+ expect(currentState?.step).toBe('WalletConnected');
582
+ // Now disconnect
583
+ store.disconnect();
584
+ expect(currentState?.step).toBe('Idle');
585
+ if (currentState?.step === 'Idle') {
586
+ expect(currentState.loading).toBe(false);
587
+ expect(currentState.wallet).toBeUndefined();
588
+ }
589
+ });
590
+ });
591
+ describe('cancel', () => {
592
+ it('should transition to Idle when cancel is called', async () => {
593
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
594
+ const store = createConnection({
595
+ walletHost: 'https://wallet.example.com',
596
+ chainInfo: defaultChainInfo,
597
+ walletConnector,
598
+ autoConnect: false,
599
+ });
600
+ let currentState;
601
+ store.subscribe((state) => {
602
+ currentState = state;
603
+ });
604
+ await store.connect();
605
+ expect(currentState?.step).toBe('MechanismToChoose');
606
+ store.cancel();
607
+ expect(currentState?.step).toBe('Idle');
608
+ });
609
+ });
610
+ describe('back', () => {
611
+ it('should transition to MechanismToChoose when back is called with MechanismToChoose', async () => {
612
+ const walletConnector = createMockWalletConnector([createMockWalletHandle(), createMockWalletHandle('Wallet2')]);
613
+ const store = createConnection({
614
+ walletHost: 'https://wallet.example.com',
615
+ chainInfo: defaultChainInfo,
616
+ walletConnector,
617
+ autoConnect: false,
618
+ });
619
+ // Wait for wallets to be announced
620
+ vi.advanceTimersByTime(50);
621
+ let currentState;
622
+ store.subscribe((state) => {
623
+ currentState = state;
624
+ });
625
+ await store.connect({ type: 'wallet' });
626
+ expect(currentState?.step).toBe('WalletToChoose');
627
+ store.back('MechanismToChoose');
628
+ expect(currentState?.step).toBe('MechanismToChoose');
629
+ });
630
+ it('should transition to Idle when back is called with Idle', async () => {
631
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
632
+ const store = createConnection({
633
+ walletHost: 'https://wallet.example.com',
634
+ chainInfo: defaultChainInfo,
635
+ walletConnector,
636
+ autoConnect: false,
637
+ });
638
+ let currentState;
639
+ store.subscribe((state) => {
640
+ currentState = state;
641
+ });
642
+ await store.connect();
643
+ expect(currentState?.step).toBe('MechanismToChoose');
644
+ store.back('Idle');
645
+ expect(currentState?.step).toBe('Idle');
646
+ });
647
+ it('should transition to WalletToChoose when back is called with WalletToChoose', async () => {
648
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
649
+ const store = createConnection({
650
+ walletHost: 'https://wallet.example.com',
651
+ chainInfo: defaultChainInfo,
652
+ walletConnector,
653
+ autoConnect: false,
654
+ });
655
+ let currentState;
656
+ store.subscribe((state) => {
657
+ currentState = state;
658
+ });
659
+ await store.connect();
660
+ store.back('WalletToChoose');
661
+ expect(currentState?.step).toBe('WalletToChoose');
662
+ });
663
+ });
664
+ describe('connectToAddress', () => {
665
+ it('should connect to a specific address when wallet is connected', async () => {
666
+ const mockHandle = createMockWalletHandle('MockWallet', [
667
+ '0xuser1',
668
+ '0xuser2',
669
+ ]);
670
+ const walletConnector = createMockWalletConnector([mockHandle]);
671
+ const store = createConnection({
672
+ walletHost: 'https://wallet.example.com',
673
+ chainInfo: defaultChainInfo,
674
+ walletConnector,
675
+ autoConnect: false,
676
+ alwaysUseCurrentAccount: false,
677
+ });
678
+ // Wait for wallet to be announced
679
+ vi.advanceTimersByTime(50);
680
+ let currentState;
681
+ store.subscribe((state) => {
682
+ currentState = state;
683
+ });
684
+ const connectPromise = store.connect({ type: 'wallet' });
685
+ await vi.advanceTimersByTimeAsync(100);
686
+ await connectPromise;
687
+ expect(currentState?.step).toBe('ChooseWalletAccount');
688
+ // Now choose a specific address
689
+ store.connectToAddress('0xuser2');
690
+ await vi.advanceTimersByTimeAsync(100);
691
+ expect(currentState?.step).toBe('WalletConnected');
692
+ if (currentState?.step === 'WalletConnected') {
693
+ expect(currentState.mechanism.address).toBe('0xuser2');
694
+ }
695
+ });
696
+ it('should throw error when no wallet is connected', () => {
697
+ const walletConnector = createMockWalletConnector([]);
698
+ const store = createConnection({
699
+ walletHost: 'https://wallet.example.com',
700
+ chainInfo: defaultChainInfo,
701
+ walletConnector,
702
+ autoConnect: false,
703
+ });
704
+ expect(() => store.connectToAddress('0xuser')).toThrow('need to be using a wallet');
705
+ });
706
+ });
707
+ describe('store.isTargetStepReached', () => {
708
+ it('should correctly check if target step is reached', async () => {
709
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
710
+ const walletConnector = createMockWalletConnector([mockHandle]);
711
+ const store = createConnection({
712
+ targetStep: 'WalletConnected',
713
+ chainInfo: defaultChainInfo,
714
+ walletConnector,
715
+ autoConnect: false,
716
+ });
717
+ // Wait for wallet to be announced
718
+ vi.advanceTimersByTime(50);
719
+ let currentState;
720
+ store.subscribe((state) => {
721
+ currentState = state;
722
+ });
723
+ // Initially not reached
724
+ expect(store.isTargetStepReached(currentState)).toBe(false);
725
+ // Connect
726
+ const connectPromise = store.connect({ type: 'wallet' });
727
+ await vi.advanceTimersByTimeAsync(100);
728
+ await connectPromise;
729
+ // Now should be reached
730
+ expect(store.isTargetStepReached(currentState)).toBe(true);
731
+ });
732
+ });
733
+ describe('chainId handling', () => {
734
+ it('should detect invalid chainId when wallet chain differs from configured chain', async () => {
735
+ // Create a wallet on chain 5 (Goerli) while our config is for chain 1 (Mainnet)
736
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123'], '0x5');
737
+ const walletConnector = createMockWalletConnector([mockHandle]);
738
+ const store = createConnection({
739
+ walletHost: 'https://wallet.example.com',
740
+ chainInfo: defaultChainInfo, // Chain ID 1
741
+ walletConnector,
742
+ autoConnect: false,
743
+ });
744
+ // Wait for wallet to be announced
745
+ vi.advanceTimersByTime(50);
746
+ let currentState;
747
+ store.subscribe((state) => {
748
+ currentState = state;
749
+ });
750
+ const connectPromise = store.connect({ type: 'wallet' });
751
+ await vi.advanceTimersByTimeAsync(100);
752
+ await connectPromise;
753
+ expect(currentState?.step).toBe('WalletConnected');
754
+ if (currentState?.step === 'WalletConnected') {
755
+ expect(currentState.wallet.invalidChainId).toBe(true);
756
+ expect(currentState.wallet.chainId).toBe('5');
757
+ }
758
+ });
759
+ it('should not mark chainId as invalid when chains match', async () => {
760
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123'], '0x1');
761
+ const walletConnector = createMockWalletConnector([mockHandle]);
762
+ const store = createConnection({
763
+ walletHost: 'https://wallet.example.com',
764
+ chainInfo: defaultChainInfo, // Chain ID 1
765
+ walletConnector,
766
+ autoConnect: false,
767
+ });
768
+ // Wait for wallet to be announced
769
+ vi.advanceTimersByTime(50);
770
+ let currentState;
771
+ store.subscribe((state) => {
772
+ currentState = state;
773
+ });
774
+ const connectPromise = store.connect({ type: 'wallet' });
775
+ await vi.advanceTimersByTimeAsync(100);
776
+ await connectPromise;
777
+ expect(currentState?.step).toBe('WalletConnected');
778
+ if (currentState?.step === 'WalletConnected') {
779
+ expect(currentState.wallet.invalidChainId).toBe(false);
780
+ expect(currentState.wallet.chainId).toBe('1');
781
+ }
782
+ });
783
+ });
784
+ describe('WalletConnected targetStep', () => {
785
+ it('should auto-connect with wallet mechanism when targetStep is WalletConnected', async () => {
786
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
787
+ const walletConnector = createMockWalletConnector([mockHandle]);
788
+ const store = createConnection({
789
+ targetStep: 'WalletConnected',
790
+ chainInfo: defaultChainInfo,
791
+ walletConnector,
792
+ autoConnect: false,
793
+ });
794
+ // Wait for wallet to be announced
795
+ vi.advanceTimersByTime(50);
796
+ let currentState;
797
+ store.subscribe((state) => {
798
+ currentState = state;
799
+ });
800
+ // When calling connect() without mechanism, should default to wallet
801
+ // Specify wallet name to avoid WalletToChoose step
802
+ const connectPromise = store.connect({ type: 'wallet', name: 'MockWallet' });
803
+ await vi.advanceTimersByTimeAsync(100);
804
+ await connectPromise;
805
+ // For single wallet with specified name, should go to WalletConnected directly
806
+ expect(currentState?.step).toBe('WalletConnected');
807
+ });
808
+ });
809
+ describe('ensureConnected', () => {
810
+ it('should resolve when already connected to target step', async () => {
811
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
812
+ const walletConnector = createMockWalletConnector([mockHandle]);
813
+ const store = createConnection({
814
+ targetStep: 'WalletConnected',
815
+ chainInfo: defaultChainInfo,
816
+ walletConnector,
817
+ autoConnect: false,
818
+ });
819
+ // Wait for wallet to be announced
820
+ vi.advanceTimersByTime(50);
821
+ // Connect first
822
+ const connectPromise = store.connect({ type: 'wallet' });
823
+ await vi.advanceTimersByTimeAsync(100);
824
+ await connectPromise;
825
+ // Now ensureConnected should resolve immediately
826
+ const result = await store.ensureConnected();
827
+ expect(result.step).toBe('WalletConnected');
828
+ });
829
+ it('should start connection if not yet connected', async () => {
830
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
831
+ const walletConnector = createMockWalletConnector([mockHandle]);
832
+ const store = createConnection({
833
+ targetStep: 'WalletConnected',
834
+ chainInfo: defaultChainInfo,
835
+ walletConnector,
836
+ autoConnect: false,
837
+ });
838
+ // Wait for wallet to be announced
839
+ vi.advanceTimersByTime(50);
840
+ // ensureConnected should start connection and resolve
841
+ const ensurePromise = store.ensureConnected();
842
+ await vi.advanceTimersByTimeAsync(200);
843
+ const result = await ensurePromise;
844
+ expect(result.step).toBe('WalletConnected');
845
+ });
846
+ it('should reject when connection is cancelled', async () => {
847
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
848
+ const walletConnector = createMockWalletConnector([mockHandle]);
849
+ const store = createConnection({
850
+ targetStep: 'WalletConnected',
851
+ chainInfo: defaultChainInfo,
852
+ walletConnector,
853
+ autoConnect: false,
854
+ });
855
+ // Wait for wallet to be announced
856
+ vi.advanceTimersByTime(50);
857
+ // Start ensureConnected
858
+ const ensurePromise = store.ensureConnected();
859
+ // Cancel the connection
860
+ store.cancel();
861
+ await expect(ensurePromise).rejects.toThrow('Connection cancelled');
862
+ });
863
+ });
864
+ describe('walletHost requirement', () => {
865
+ it('should not require walletHost for WalletConnected targetStep', () => {
866
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
867
+ // This should not throw
868
+ const store = createConnection({
869
+ targetStep: 'WalletConnected',
870
+ chainInfo: defaultChainInfo,
871
+ walletConnector,
872
+ });
873
+ expect(store).toBeDefined();
874
+ });
875
+ it('should not require walletHost when walletOnly is true', () => {
876
+ const walletConnector = createMockWalletConnector([createMockWalletHandle()]);
877
+ // This should not throw
878
+ const store = createConnection({
879
+ walletOnly: true,
880
+ chainInfo: defaultChainInfo,
881
+ walletConnector,
882
+ });
883
+ expect(store).toBeDefined();
884
+ });
885
+ it('should throw when using popup-based auth without walletHost', async () => {
886
+ const walletConnector = createMockWalletConnector([]);
887
+ const store = createConnection({
888
+ walletHost: undefined, // Force undefined to test
889
+ chainInfo: defaultChainInfo,
890
+ walletConnector,
891
+ autoConnect: false,
892
+ });
893
+ // Attempting popup-based auth should fail
894
+ await expect(store.connect({ type: 'email', email: 'test@example.com', mode: 'otp' })).rejects.toThrow('walletHost is required for popup-based authentication');
895
+ });
896
+ });
897
+ });
898
+ describe('wallet state tracking', () => {
899
+ beforeEach(() => {
900
+ localStorage.clear();
901
+ sessionStorage.clear();
902
+ vi.useFakeTimers();
903
+ });
904
+ afterEach(() => {
905
+ vi.clearAllMocks();
906
+ vi.useRealTimers();
907
+ });
908
+ it('should track wallet status correctly', async () => {
909
+ const mockHandle = createMockWalletHandle('MockWallet', ['0xuser123']);
910
+ const walletConnector = createMockWalletConnector([mockHandle]);
911
+ const store = createConnection({
912
+ walletHost: 'https://wallet.example.com',
913
+ chainInfo: defaultChainInfo,
914
+ walletConnector,
915
+ autoConnect: false,
916
+ });
917
+ // Wait for wallet to be announced
918
+ vi.advanceTimersByTime(50);
919
+ let currentState;
920
+ store.subscribe((state) => {
921
+ currentState = state;
922
+ });
923
+ const connectPromise = store.connect({ type: 'wallet' });
924
+ await vi.advanceTimersByTimeAsync(100);
925
+ await connectPromise;
926
+ expect(currentState?.step).toBe('WalletConnected');
927
+ if (currentState?.step === 'WalletConnected') {
928
+ expect(currentState.wallet.status).toBe('connected');
929
+ expect(currentState.wallet.accounts).toContain('0xuser123');
930
+ expect(currentState.wallet.switchingChain).toBe(false);
931
+ }
932
+ });
933
+ it('should populate wallets array when wallets are announced', async () => {
934
+ const handle1 = createMockWalletHandle('Wallet1', ['0x111']);
935
+ const handle2 = createMockWalletHandle('Wallet2', ['0x222']);
936
+ const walletConnector = createMockWalletConnector([handle1, handle2]);
937
+ const store = createConnection({
938
+ walletHost: 'https://wallet.example.com',
939
+ chainInfo: defaultChainInfo,
940
+ walletConnector,
941
+ autoConnect: false,
942
+ });
943
+ let currentState;
944
+ store.subscribe((state) => {
945
+ currentState = state;
946
+ });
947
+ // Wait for wallets to be announced
948
+ vi.advanceTimersByTime(50);
949
+ expect(currentState?.wallets.length).toBe(2);
950
+ expect(currentState?.wallets.map((w) => w.info.name)).toContain('Wallet1');
951
+ expect(currentState?.wallets.map((w) => w.info.name)).toContain('Wallet2');
952
+ });
953
+ });
954
+ //# sourceMappingURL=index.test.js.map