@tomo-inc/inject-providers 0.0.16 → 0.0.18

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,1362 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import messages from "../evm/messages";
3
+ import { EvmProvider, initializeEvmProvider, setGlobalProvider } from "../evm/metamask";
4
+ import { EMITTED_NOTIFICATIONS } from "../evm/utils";
5
+ import { IProductInfo } from "../types";
6
+ import * as utils from "../utils/index";
7
+
8
+ describe("EvmProvider", () => {
9
+ let provider: EvmProvider;
10
+ let mockSendRequest: (chainType: string, data: any) => void;
11
+ let mockOnResponse: any;
12
+ let productInfo: IProductInfo;
13
+
14
+ beforeEach(() => {
15
+ mockSendRequest = vi.fn() as any;
16
+ mockOnResponse = vi.fn().mockResolvedValue({ data: null });
17
+
18
+ productInfo = {
19
+ name: "Test Wallet",
20
+ rdns: "com.test.wallet",
21
+ icon: "test-icon.png",
22
+ };
23
+
24
+ // Mock getDappInfo to prevent initialization errors
25
+ vi.spyOn(utils, "getDappInfo").mockResolvedValue({
26
+ origin: "https://test.com",
27
+ title: "Test",
28
+ desc: "Test desc",
29
+ favicon: "",
30
+ });
31
+
32
+ // Mock document.body to prevent innerText errors
33
+ const bodyElement = document.createElement("body");
34
+ Object.defineProperty(bodyElement, "innerText", {
35
+ value: "",
36
+ writable: true,
37
+ configurable: true,
38
+ });
39
+ Object.defineProperty(document, "body", {
40
+ value: bodyElement,
41
+ writable: true,
42
+ configurable: true,
43
+ });
44
+ });
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ // Clean up window.ethereum
49
+ delete (window as any).ethereum;
50
+ });
51
+
52
+ describe("constructor", () => {
53
+ it("should create EvmProvider instance with valid options", () => {
54
+ provider = new EvmProvider(productInfo, {
55
+ sendRequest: mockSendRequest,
56
+ onResponse: mockOnResponse,
57
+ logger: console,
58
+ maxEventListeners: 100,
59
+ shouldSendMetadata: true,
60
+ });
61
+
62
+ expect(provider).toBeInstanceOf(EvmProvider);
63
+ expect(provider.name).toBe("Test Wallet");
64
+ expect(provider.icon).toBe("test-icon.png");
65
+ });
66
+
67
+ it("should throw error for invalid maxEventListeners", () => {
68
+ expect(() => {
69
+ new EvmProvider(productInfo, {
70
+ sendRequest: mockSendRequest,
71
+ onResponse: mockOnResponse,
72
+ maxEventListeners: "invalid" as any,
73
+ shouldSendMetadata: true,
74
+ });
75
+ }).toThrow(messages.errors.invalidOptions("invalid", true));
76
+ });
77
+
78
+ it("should throw error for invalid shouldSendMetadata", () => {
79
+ expect(() => {
80
+ new EvmProvider(productInfo, {
81
+ sendRequest: mockSendRequest,
82
+ onResponse: mockOnResponse,
83
+ maxEventListeners: 100,
84
+ shouldSendMetadata: "invalid" as any,
85
+ });
86
+ }).toThrow(messages.errors.invalidOptions(100, "invalid"));
87
+ });
88
+
89
+ it("should use default logger if not provided", () => {
90
+ provider = new EvmProvider(productInfo, {
91
+ sendRequest: mockSendRequest,
92
+ onResponse: mockOnResponse,
93
+ });
94
+
95
+ expect(provider).toBeInstanceOf(EvmProvider);
96
+ });
97
+ });
98
+
99
+ describe("request", () => {
100
+ beforeEach(() => {
101
+ provider = new EvmProvider(productInfo, {
102
+ sendRequest: mockSendRequest,
103
+ onResponse: mockOnResponse,
104
+ });
105
+ });
106
+
107
+ it("should throw error if method is not provided", async () => {
108
+ await expect(provider.request({ method: "" } as any)).rejects.toThrow();
109
+ });
110
+
111
+ it("should throw error if method is not a string", async () => {
112
+ await expect(provider.request({ method: 123 } as any)).rejects.toThrow();
113
+ });
114
+
115
+ it("should throw error if args is not an object", async () => {
116
+ await expect(provider.request(null as any)).rejects.toThrow();
117
+ await expect(provider.request([] as any)).rejects.toThrow();
118
+ });
119
+
120
+ it("should validate wallet_addEthereumChain params", async () => {
121
+ await expect(
122
+ provider.request({
123
+ method: "wallet_addEthereumChain",
124
+ params: [],
125
+ }),
126
+ ).rejects.toThrow();
127
+
128
+ await expect(
129
+ provider.request({
130
+ method: "wallet_addEthereumChain",
131
+ params: [{}],
132
+ }),
133
+ ).rejects.toThrow();
134
+
135
+ await expect(
136
+ provider.request({
137
+ method: "wallet_addEthereumChain",
138
+ params: [{ chainName: "Test" }],
139
+ }),
140
+ ).rejects.toThrow();
141
+
142
+ mockOnResponse.mockResolvedValue({
143
+ data: true,
144
+ method: "wallet_addEthereumChain",
145
+ });
146
+
147
+ await provider.request({
148
+ method: "wallet_addEthereumChain",
149
+ params: [
150
+ {
151
+ chainName: "Test Chain",
152
+ rpcUrls: ["https://test.com"],
153
+ chainId: "0x1",
154
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
155
+ },
156
+ ],
157
+ });
158
+
159
+ expect(mockSendRequest).toHaveBeenCalled();
160
+ });
161
+
162
+ it("should validate personal_sign params", async () => {
163
+ await expect(
164
+ provider.request({
165
+ method: "personal_sign",
166
+ params: [],
167
+ }),
168
+ ).rejects.toThrow();
169
+
170
+ await expect(
171
+ provider.request({
172
+ method: "personal_sign",
173
+ params: ["0x123"],
174
+ }),
175
+ ).rejects.toThrow();
176
+
177
+ await expect(
178
+ provider.request({
179
+ method: "personal_sign",
180
+ params: [null, "0x123"],
181
+ }),
182
+ ).rejects.toThrow();
183
+
184
+ mockOnResponse.mockResolvedValue({
185
+ data: "0xsignature",
186
+ method: "personal_sign",
187
+ });
188
+
189
+ await provider.request({
190
+ method: "personal_sign",
191
+ params: ["0x123", "0x456"],
192
+ });
193
+
194
+ expect(mockSendRequest).toHaveBeenCalled();
195
+ });
196
+
197
+ it("should validate wallet_watchAsset params", async () => {
198
+ await expect(
199
+ provider.request({
200
+ method: "wallet_watchAsset",
201
+ params: { type: "ERC20" },
202
+ }),
203
+ ).rejects.toThrow();
204
+
205
+ await expect(
206
+ provider.request({
207
+ method: "wallet_watchAsset",
208
+ params: {
209
+ type: "ERC20",
210
+ options: {},
211
+ },
212
+ }),
213
+ ).rejects.toThrow();
214
+
215
+ await expect(
216
+ provider.request({
217
+ method: "wallet_watchAsset",
218
+ params: {
219
+ type: "ERC20",
220
+ options: { address: "0x123" },
221
+ },
222
+ }),
223
+ ).rejects.toThrow();
224
+
225
+ mockOnResponse.mockResolvedValue({
226
+ data: true,
227
+ method: "wallet_watchAsset",
228
+ });
229
+
230
+ await provider.request({
231
+ method: "wallet_watchAsset",
232
+ params: {
233
+ type: "ERC20",
234
+ options: {
235
+ address: "0x123",
236
+ symbol: "TST",
237
+ decimals: 18,
238
+ },
239
+ },
240
+ });
241
+
242
+ expect(mockSendRequest).toHaveBeenCalled();
243
+ });
244
+
245
+ it("should validate params type", async () => {
246
+ await expect(
247
+ provider.request({
248
+ method: "eth_getBalance",
249
+ params: "invalid" as any,
250
+ }),
251
+ ).rejects.toThrow();
252
+
253
+ await expect(
254
+ provider.request({
255
+ method: "eth_getBalance",
256
+ params: null as any,
257
+ }),
258
+ ).rejects.toThrow();
259
+ });
260
+
261
+ it("should handle _wallet_getProviderState response", async () => {
262
+ const accounts = ["0x123", "0x456"];
263
+ mockOnResponse.mockResolvedValue({
264
+ data: { accounts },
265
+ method: "_wallet_getProviderState",
266
+ });
267
+
268
+ const accountsChangedSpy = vi.fn();
269
+ provider.on("accountsChanged", accountsChangedSpy);
270
+
271
+ await provider.request({
272
+ method: "_wallet_getProviderState",
273
+ });
274
+
275
+ // Wait for async operations
276
+ await new Promise((resolve) => setTimeout(resolve, 100));
277
+
278
+ expect(mockSendRequest).toHaveBeenCalled();
279
+ });
280
+
281
+ it("should handle wallet_revokePermissions response", async () => {
282
+ mockOnResponse.mockResolvedValue({
283
+ data: true,
284
+ method: "wallet_revokePermissions",
285
+ });
286
+
287
+ const accountsChangedSpy = vi.fn();
288
+ provider.on("accountsChanged", accountsChangedSpy);
289
+
290
+ await provider.request({
291
+ method: "wallet_revokePermissions",
292
+ });
293
+
294
+ // Wait for async operations
295
+ await new Promise((resolve) => setTimeout(resolve, 100));
296
+
297
+ expect(mockSendRequest).toHaveBeenCalled();
298
+ });
299
+
300
+ it("should handle connect methods response", async () => {
301
+ const accounts = ["0x123"];
302
+ mockOnResponse.mockResolvedValue({
303
+ data: accounts,
304
+ method: "eth_requestAccounts",
305
+ });
306
+
307
+ const accountsChangedSpy = vi.fn();
308
+ provider.on("accountsChanged", accountsChangedSpy);
309
+
310
+ await provider.request({
311
+ method: "eth_requestAccounts",
312
+ });
313
+
314
+ // Wait for async operations
315
+ await new Promise((resolve) => setTimeout(resolve, 100));
316
+
317
+ expect(mockSendRequest).toHaveBeenCalled();
318
+ });
319
+
320
+ it("should handle wallet_switchEthereumChain response", async () => {
321
+ const chainId = "0x5";
322
+ mockOnResponse.mockResolvedValue({
323
+ data: chainId,
324
+ method: "wallet_switchEthereumChain",
325
+ });
326
+
327
+ const chainChangedSpy = vi.fn();
328
+ provider.on("chainChanged", chainChangedSpy);
329
+
330
+ await provider.request({
331
+ method: "wallet_switchEthereumChain",
332
+ params: [{ chainId }],
333
+ });
334
+
335
+ // Wait for async operations
336
+ await new Promise((resolve) => setTimeout(resolve, 100));
337
+
338
+ expect(mockSendRequest).toHaveBeenCalled();
339
+ });
340
+
341
+ it("should handle eth_chainId response", async () => {
342
+ const chainId = "0x1";
343
+ mockOnResponse.mockResolvedValue({
344
+ data: chainId,
345
+ method: "eth_chainId",
346
+ });
347
+
348
+ const chainChangedSpy = vi.fn();
349
+ provider.on("chainChanged", chainChangedSpy);
350
+
351
+ await provider.request({
352
+ method: "eth_chainId",
353
+ });
354
+
355
+ // Wait for async operations
356
+ await new Promise((resolve) => setTimeout(resolve, 100));
357
+
358
+ expect(mockSendRequest).toHaveBeenCalled();
359
+ });
360
+ });
361
+
362
+ describe("connect", () => {
363
+ beforeEach(() => {
364
+ provider = new EvmProvider(productInfo, {
365
+ sendRequest: mockSendRequest,
366
+ onResponse: mockOnResponse,
367
+ });
368
+ });
369
+
370
+ it("should emit connect event when accounts are returned", async () => {
371
+ const connectSpy = vi.spyOn(provider, "emit");
372
+ // Mock request to return accounts
373
+ vi.spyOn(provider, "request").mockResolvedValue(["0x123"] as any);
374
+
375
+ await provider.connect();
376
+
377
+ expect(connectSpy).toHaveBeenCalledWith("connect", {});
378
+ });
379
+ });
380
+
381
+ describe("disconnect", () => {
382
+ beforeEach(() => {
383
+ provider = new EvmProvider(productInfo, {
384
+ sendRequest: mockSendRequest,
385
+ onResponse: mockOnResponse,
386
+ });
387
+ });
388
+
389
+ it("should emit disconnect event when disconnected", async () => {
390
+ const disconnectSpy = vi.spyOn(provider, "emit");
391
+ // Mock request to return disconnected state
392
+ vi.spyOn(provider, "request").mockResolvedValue({ isConnected: false } as any);
393
+
394
+ await provider.disconnect();
395
+
396
+ expect(disconnectSpy).toHaveBeenCalledWith("disconnect", null);
397
+ });
398
+ });
399
+
400
+ describe("isConnected", () => {
401
+ beforeEach(() => {
402
+ provider = new EvmProvider(productInfo, {
403
+ sendRequest: mockSendRequest,
404
+ onResponse: mockOnResponse,
405
+ });
406
+ });
407
+
408
+ it("should return false when not connected", () => {
409
+ expect(provider.isConnected()).toBe(false);
410
+ });
411
+
412
+ it("should return true when connected", async () => {
413
+ // Trigger connect by emitting connect event
414
+ provider.emit("connect", { chainId: "0x1" });
415
+ expect(provider.isConnected()).toBe(true);
416
+ });
417
+ });
418
+
419
+ describe("sendAsync", () => {
420
+ beforeEach(() => {
421
+ provider = new EvmProvider(productInfo, {
422
+ sendRequest: mockSendRequest,
423
+ onResponse: mockOnResponse,
424
+ });
425
+ });
426
+
427
+ it("should call _rpcRequest with callback", () => {
428
+ const callback = vi.fn();
429
+ const payload = {
430
+ jsonrpc: "2.0" as const,
431
+ id: 1,
432
+ method: "eth_getBalance",
433
+ params: ["0x123"],
434
+ };
435
+
436
+ // Mock _rpcEngine.handle
437
+ const mockHandle = vi.fn();
438
+ (provider as any)._rpcEngine = {
439
+ handle: mockHandle,
440
+ };
441
+
442
+ provider.sendAsync(payload, callback);
443
+
444
+ expect(mockHandle).toHaveBeenCalledWith(payload, callback);
445
+ });
446
+ });
447
+
448
+ describe("event listeners", () => {
449
+ beforeEach(() => {
450
+ provider = new EvmProvider(productInfo, {
451
+ sendRequest: mockSendRequest,
452
+ onResponse: mockOnResponse,
453
+ logger: {
454
+ log: vi.fn(),
455
+ warn: vi.fn(),
456
+ error: vi.fn(),
457
+ debug: vi.fn(),
458
+ info: vi.fn(),
459
+ trace: vi.fn(),
460
+ },
461
+ });
462
+ });
463
+
464
+ it("should warn on deprecated addListener", () => {
465
+ const listener = vi.fn();
466
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
467
+
468
+ provider.addListener("close", listener);
469
+
470
+ expect(warnSpy).toHaveBeenCalled();
471
+ });
472
+
473
+ it("should warn on deprecated once", () => {
474
+ const listener = vi.fn();
475
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
476
+
477
+ provider.once("data", listener);
478
+
479
+ expect(warnSpy).toHaveBeenCalled();
480
+ });
481
+
482
+ it("should warn on deprecated prependListener", () => {
483
+ const listener = vi.fn();
484
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
485
+
486
+ provider.prependListener("networkChanged", listener);
487
+
488
+ expect(warnSpy).toHaveBeenCalled();
489
+ });
490
+
491
+ it("should warn on deprecated prependOnceListener", () => {
492
+ const listener = vi.fn();
493
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
494
+
495
+ provider.prependOnceListener("notification", listener);
496
+
497
+ expect(warnSpy).toHaveBeenCalled();
498
+ });
499
+
500
+ it("should not warn twice for same event", () => {
501
+ const listener = vi.fn();
502
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
503
+
504
+ provider.addListener("close", listener);
505
+ provider.addListener("close", listener);
506
+
507
+ expect(warnSpy).toHaveBeenCalledTimes(1);
508
+ });
509
+ });
510
+
511
+ describe("JSON-RPC notifications", () => {
512
+ beforeEach(() => {
513
+ provider = new EvmProvider(productInfo, {
514
+ sendRequest: mockSendRequest,
515
+ onResponse: mockOnResponse,
516
+ });
517
+ });
518
+
519
+ it("should handle wallet_accountsChanged notification", async () => {
520
+ // Wait for initialization to complete
521
+ await new Promise((resolve) => setTimeout(resolve, 200));
522
+
523
+ const accountsChangedSpy = vi.fn();
524
+ provider.on("accountsChanged", accountsChangedSpy);
525
+
526
+ // Access the internal jsonRpcConnection through _rpcEngine
527
+ const rpcEngine = (provider as any)._rpcEngine;
528
+ const middlewares = (rpcEngine as any)._middleware || [];
529
+ const streamMiddleware = middlewares.find((m: any) => m && m.stream);
530
+
531
+ if (streamMiddleware && streamMiddleware.events) {
532
+ streamMiddleware.events.emit("notification", {
533
+ method: "wallet_accountsChanged",
534
+ params: [["0x123", "0x456"]],
535
+ });
536
+
537
+ await new Promise((resolve) => setTimeout(resolve, 100));
538
+ expect(accountsChangedSpy).toHaveBeenCalled();
539
+ }
540
+ });
541
+
542
+ it("should handle wallet_unlockStateChanged notification", async () => {
543
+ await new Promise((resolve) => setTimeout(resolve, 200));
544
+
545
+ const accountsChangedSpy = vi.fn();
546
+ provider.on("accountsChanged", accountsChangedSpy);
547
+
548
+ const rpcEngine = (provider as any)._rpcEngine;
549
+ const middlewares = (rpcEngine as any)._middleware || [];
550
+ const streamMiddleware = middlewares.find((m: any) => m && m.stream);
551
+
552
+ if (streamMiddleware && streamMiddleware.events) {
553
+ streamMiddleware.events.emit("notification", {
554
+ method: "wallet_unlockStateChanged",
555
+ params: [{ accounts: ["0x123"], isUnlocked: true }],
556
+ });
557
+
558
+ await new Promise((resolve) => setTimeout(resolve, 100));
559
+ }
560
+ });
561
+
562
+ it("should handle wallet_chainChanged notification", async () => {
563
+ await new Promise((resolve) => setTimeout(resolve, 200));
564
+
565
+ const chainChangedSpy = vi.fn();
566
+ provider.on("chainChanged", chainChangedSpy);
567
+
568
+ const rpcEngine = (provider as any)._rpcEngine;
569
+ const middlewares = (rpcEngine as any)._middleware || [];
570
+ const streamMiddleware = middlewares.find((m: any) => m && m.stream);
571
+
572
+ if (streamMiddleware && streamMiddleware.events) {
573
+ streamMiddleware.events.emit("notification", {
574
+ method: "wallet_chainChanged",
575
+ params: [{ chainId: "0x5" }],
576
+ });
577
+
578
+ await new Promise((resolve) => setTimeout(resolve, 100));
579
+ }
580
+ });
581
+
582
+ it("should handle EMITTED_NOTIFICATIONS", async () => {
583
+ await new Promise((resolve) => setTimeout(resolve, 200));
584
+
585
+ const dataSpy = vi.fn();
586
+ const messageSpy = vi.fn();
587
+ const notificationSpy = vi.fn();
588
+
589
+ provider.on("data", dataSpy);
590
+ provider.on("message", messageSpy);
591
+ provider.on("notification", notificationSpy);
592
+
593
+ const rpcEngine = (provider as any)._rpcEngine;
594
+ const middlewares = (rpcEngine as any)._middleware || [];
595
+ const streamMiddleware = middlewares.find((m: any) => m && m.stream);
596
+
597
+ if (streamMiddleware && streamMiddleware.events) {
598
+ const notificationMethod = EMITTED_NOTIFICATIONS[0];
599
+ streamMiddleware.events.emit("notification", {
600
+ method: notificationMethod,
601
+ params: { result: "test" },
602
+ });
603
+
604
+ await new Promise((resolve) => setTimeout(resolve, 100));
605
+ }
606
+ });
607
+
608
+ it("should handle WALLET_STREAM_FAILURE", async () => {
609
+ await new Promise((resolve) => setTimeout(resolve, 200));
610
+
611
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
612
+
613
+ const rpcEngine = (provider as any)._rpcEngine;
614
+ const middlewares = (rpcEngine as any)._middleware || [];
615
+ const streamMiddleware = middlewares.find((m: any) => m && m.stream);
616
+
617
+ if (streamMiddleware && streamMiddleware.events) {
618
+ streamMiddleware.events.emit("notification", {
619
+ method: "WALLET_STREAM_FAILURE",
620
+ params: {},
621
+ });
622
+
623
+ await new Promise((resolve) => setTimeout(resolve, 100));
624
+ }
625
+
626
+ consoleErrorSpy.mockRestore();
627
+ });
628
+ });
629
+
630
+ describe("subscribeWalletEventsCallback", () => {
631
+ beforeEach(async () => {
632
+ // Mock the initial state request
633
+ mockOnResponse.mockResolvedValueOnce({
634
+ data: {
635
+ accounts: [],
636
+ chainId: "0x1",
637
+ isUnlocked: false,
638
+ isConnected: false,
639
+ },
640
+ method: "wallet_getProviderState",
641
+ });
642
+
643
+ provider = new EvmProvider(productInfo, {
644
+ sendRequest: mockSendRequest,
645
+ onResponse: mockOnResponse,
646
+ });
647
+ // Wait for _initializeState to complete and set up window.addEventListener
648
+ await new Promise((resolve) => {
649
+ provider.once("_initialized", resolve);
650
+ // Fallback timeout
651
+ setTimeout(resolve, 500);
652
+ });
653
+ });
654
+
655
+ it("should handle accountsChanged message", async () => {
656
+ const accountsChangedSpy = vi.fn();
657
+ provider.on("accountsChanged", accountsChangedSpy);
658
+
659
+ // Simulate window message event
660
+ // Note: chainType is "evm" (lowercase) as defined by ChainTypes.EVM
661
+ const event = new MessageEvent("message", {
662
+ data: {
663
+ type: "subscribeWalletEvents",
664
+ method: "accountsChanged",
665
+ data: {
666
+ evm: [{ address: "0x123" }, { address: "0x456" }],
667
+ },
668
+ },
669
+ });
670
+
671
+ window.dispatchEvent(event);
672
+
673
+ await new Promise((resolve) => setTimeout(resolve, 100));
674
+
675
+ expect(accountsChangedSpy).toHaveBeenCalled();
676
+ });
677
+
678
+ it("should handle chainChanged message", async () => {
679
+ const chainChangedSpy = vi.fn();
680
+ provider.on("chainChanged", chainChangedSpy);
681
+
682
+ // Note: chainType is "evm" (lowercase), so type should start with "evm:"
683
+ const event = new MessageEvent("message", {
684
+ data: {
685
+ type: "subscribeWalletEvents",
686
+ method: "chainChanged",
687
+ data: {
688
+ chainId: "5",
689
+ type: "evm:5",
690
+ },
691
+ },
692
+ });
693
+
694
+ window.dispatchEvent(event);
695
+
696
+ await new Promise((resolve) => setTimeout(resolve, 100));
697
+
698
+ expect(chainChangedSpy).toHaveBeenCalled();
699
+ });
700
+
701
+ it("should ignore non-subscribeWalletEvents messages", () => {
702
+ const accountsChangedSpy = vi.fn();
703
+ provider.on("accountsChanged", accountsChangedSpy);
704
+
705
+ const event = new MessageEvent("message", {
706
+ data: {
707
+ type: "other",
708
+ method: "accountsChanged",
709
+ },
710
+ });
711
+
712
+ window.dispatchEvent(event);
713
+
714
+ expect(accountsChangedSpy).not.toHaveBeenCalled();
715
+ });
716
+ });
717
+
718
+ describe("keepAlive", () => {
719
+ let keepAliveMockSendRequest: ReturnType<typeof vi.fn>;
720
+ let keepAliveMockOnResponse: ReturnType<typeof vi.fn>;
721
+ let keepAliveSpy: ReturnType<typeof vi.fn>;
722
+
723
+ beforeEach(async () => {
724
+ vi.useFakeTimers();
725
+ keepAliveMockSendRequest = vi.fn();
726
+
727
+ // Mock keepAlive response - always resolve to allow testing
728
+ keepAliveMockOnResponse = vi.fn().mockImplementation((args: any) => {
729
+ if (args?.method === "keepAlive") {
730
+ return Promise.resolve({
731
+ data: true,
732
+ method: "keepAlive",
733
+ });
734
+ }
735
+ // For other methods, return default
736
+ return Promise.resolve({ data: null });
737
+ });
738
+
739
+ // Mock initial state request
740
+ keepAliveMockOnResponse.mockResolvedValueOnce({
741
+ data: {
742
+ accounts: [],
743
+ chainId: "0x1",
744
+ isUnlocked: false,
745
+ isConnected: false,
746
+ },
747
+ method: "wallet_getProviderState",
748
+ });
749
+
750
+ provider = new EvmProvider(productInfo, {
751
+ sendRequest: keepAliveMockSendRequest as any,
752
+ onResponse: keepAliveMockOnResponse,
753
+ });
754
+
755
+ // Spy on keepAlive method to track calls
756
+ keepAliveSpy = vi.spyOn(provider as any, "keepAlive");
757
+
758
+ // Wait for initialization to complete
759
+ await new Promise((resolve) => {
760
+ provider.once("_initialized", resolve);
761
+ // Process pending timers
762
+ vi.runAllTicks();
763
+ });
764
+ });
765
+
766
+ afterEach(() => {
767
+ vi.useRealTimers();
768
+ keepAliveSpy.mockRestore();
769
+ });
770
+
771
+ it("should call keepAlive periodically", async () => {
772
+ // Clear previous calls
773
+ keepAliveMockSendRequest.mockClear();
774
+ keepAliveSpy.mockClear();
775
+
776
+ // Fast-forward time to trigger keepAlive (1000ms interval)
777
+ // Advance by 1500ms to trigger at least one keepAlive call
778
+ vi.advanceTimersByTime(1500);
779
+
780
+ // Process only the current tick's timers, not all future timers
781
+ // This prevents infinite loop
782
+ await vi.runAllTicks();
783
+
784
+ // Verify keepAlive was called
785
+ expect(keepAliveSpy).toHaveBeenCalled();
786
+
787
+ // Verify sendRequest was called with keepAlive method
788
+ const keepAliveCalls = keepAliveMockSendRequest.mock.calls.filter((call) => call[1]?.method === "keepAlive");
789
+ expect(keepAliveCalls.length).toBeGreaterThan(0);
790
+ });
791
+ });
792
+
793
+ describe("_rpcRequest", () => {
794
+ beforeEach(() => {
795
+ provider = new EvmProvider(productInfo, {
796
+ sendRequest: mockSendRequest,
797
+ onResponse: mockOnResponse,
798
+ });
799
+ });
800
+
801
+ it("should handle single payload", () => {
802
+ const callback = vi.fn();
803
+ const payload: any = {
804
+ method: "eth_getBalance",
805
+ params: ["0x123"],
806
+ };
807
+
808
+ const mockHandle = vi.fn();
809
+ (provider as any)._rpcEngine = {
810
+ handle: mockHandle,
811
+ };
812
+
813
+ (provider as any)._rpcRequest(payload, callback);
814
+
815
+ expect(mockHandle).toHaveBeenCalled();
816
+ expect(payload.jsonrpc).toBe("2.0");
817
+ });
818
+
819
+ it("should handle payload with existing jsonrpc", () => {
820
+ const callback = vi.fn();
821
+ const payload = {
822
+ jsonrpc: "2.0",
823
+ method: "eth_getBalance",
824
+ params: ["0x123"],
825
+ };
826
+
827
+ const mockHandle = vi.fn();
828
+ (provider as any)._rpcEngine = {
829
+ handle: mockHandle,
830
+ };
831
+
832
+ (provider as any)._rpcRequest(payload, callback);
833
+
834
+ expect(mockHandle).toHaveBeenCalled();
835
+ });
836
+
837
+ it("should handle eth_accounts method", () => {
838
+ const callback = vi.fn();
839
+ const payload = {
840
+ method: "eth_accounts",
841
+ params: [],
842
+ };
843
+
844
+ const mockHandle = vi.fn((p, cb) => {
845
+ cb(null, { result: ["0x123"] });
846
+ });
847
+ (provider as any)._rpcEngine = {
848
+ handle: mockHandle,
849
+ };
850
+
851
+ const accountsChangedSpy = vi.fn();
852
+ provider.on("accountsChanged", accountsChangedSpy);
853
+
854
+ (provider as any)._rpcRequest(payload, callback);
855
+
856
+ expect(mockHandle).toHaveBeenCalled();
857
+ });
858
+
859
+ it("should handle eth_requestAccounts method", () => {
860
+ const callback = vi.fn();
861
+ const payload = {
862
+ method: "eth_requestAccounts",
863
+ params: [],
864
+ };
865
+
866
+ const mockHandle = vi.fn((p, cb) => {
867
+ cb(null, { result: ["0x123"] });
868
+ });
869
+ (provider as any)._rpcEngine = {
870
+ handle: mockHandle,
871
+ };
872
+
873
+ const accountsChangedSpy = vi.fn();
874
+ provider.on("accountsChanged", accountsChangedSpy);
875
+
876
+ (provider as any)._rpcRequest(payload, callback);
877
+
878
+ expect(mockHandle).toHaveBeenCalled();
879
+ });
880
+
881
+ it("should handle array payload", () => {
882
+ const callback = vi.fn();
883
+ const payload = [
884
+ {
885
+ jsonrpc: "2.0",
886
+ method: "eth_getBalance",
887
+ params: ["0x123"],
888
+ },
889
+ ];
890
+
891
+ const mockHandle = vi.fn();
892
+ (provider as any)._rpcEngine = {
893
+ handle: mockHandle,
894
+ };
895
+
896
+ (provider as any)._rpcRequest(payload, callback);
897
+
898
+ expect(mockHandle).toHaveBeenCalled();
899
+ });
900
+ });
901
+
902
+ describe("_handleConnect", () => {
903
+ beforeEach(() => {
904
+ provider = new EvmProvider(productInfo, {
905
+ sendRequest: mockSendRequest,
906
+ onResponse: mockOnResponse,
907
+ });
908
+ });
909
+
910
+ it("should emit connect event when not connected", () => {
911
+ const connectSpy = vi.fn();
912
+ provider.on("connect", connectSpy);
913
+
914
+ (provider as any)._handleConnect("0x1");
915
+
916
+ expect(connectSpy).toHaveBeenCalledWith({ chainId: "0x1" });
917
+ expect(provider.isConnected()).toBe(true);
918
+ });
919
+
920
+ it("should not emit connect event when already connected", () => {
921
+ provider.emit("connect", { chainId: "0x1" });
922
+ const connectSpy = vi.fn();
923
+ provider.on("connect", connectSpy);
924
+
925
+ (provider as any)._handleConnect("0x2");
926
+
927
+ // Should not emit again
928
+ expect(connectSpy).not.toHaveBeenCalled();
929
+ });
930
+ });
931
+
932
+ describe("_handleDisconnect", () => {
933
+ beforeEach(() => {
934
+ provider = new EvmProvider(productInfo, {
935
+ sendRequest: mockSendRequest,
936
+ onResponse: mockOnResponse,
937
+ });
938
+ });
939
+
940
+ it("should handle recoverable disconnect", () => {
941
+ provider.emit("connect", { chainId: "0x1" });
942
+ const disconnectSpy = vi.fn();
943
+ const closeSpy = vi.fn();
944
+
945
+ provider.on("disconnect", disconnectSpy);
946
+ provider.on("close", closeSpy);
947
+
948
+ (provider as any)._handleDisconnect(true, "Test error");
949
+
950
+ expect(disconnectSpy).toHaveBeenCalled();
951
+ expect(closeSpy).toHaveBeenCalled();
952
+ expect(provider.isConnected()).toBe(false);
953
+ });
954
+
955
+ it("should handle permanent disconnect", () => {
956
+ provider.emit("connect", { chainId: "0x1" });
957
+ provider.selectedAddress = "0x123";
958
+ provider.chainId = "0x1";
959
+ provider.networkVersion = "1";
960
+ (provider as any)._state.accounts = ["0x123"];
961
+
962
+ const disconnectSpy = vi.fn();
963
+ provider.on("disconnect", disconnectSpy);
964
+
965
+ (provider as any)._handleDisconnect(false, "Permanent error");
966
+
967
+ expect(disconnectSpy).toHaveBeenCalled();
968
+ expect(provider.chainId).toBeNull();
969
+ expect(provider.networkVersion).toBeNull();
970
+ expect(provider.selectedAddress).toBeNull();
971
+ expect((provider as any)._state.accounts).toBeNull();
972
+ expect((provider as any)._state.isPermanentlyDisconnected).toBe(true);
973
+ });
974
+
975
+ it("should handle disconnect when not connected", () => {
976
+ const disconnectSpy = vi.fn();
977
+ provider.on("disconnect", disconnectSpy);
978
+
979
+ (provider as any)._handleDisconnect(false);
980
+
981
+ expect(disconnectSpy).toHaveBeenCalled();
982
+ });
983
+ });
984
+
985
+ describe("_handleChainChanged", () => {
986
+ beforeEach(() => {
987
+ provider = new EvmProvider(productInfo, {
988
+ sendRequest: mockSendRequest,
989
+ onResponse: mockOnResponse,
990
+ });
991
+ });
992
+
993
+ it("should handle valid chain change", async () => {
994
+ // Wait for initialization
995
+ await new Promise((resolve) => setTimeout(resolve, 100));
996
+
997
+ const chainChangedSpy = vi.fn();
998
+ provider.on("chainChanged", chainChangedSpy);
999
+
1000
+ (provider as any)._handleChainChanged({ chainId: "0x5", isConnected: true });
1001
+
1002
+ expect(provider.chainId).toBe("0x5");
1003
+ expect(chainChangedSpy).toHaveBeenCalledWith("0x5");
1004
+ });
1005
+
1006
+ it("should not emit if chainId is the same", async () => {
1007
+ await new Promise((resolve) => setTimeout(resolve, 100));
1008
+
1009
+ provider.chainId = "0x1";
1010
+ const chainChangedSpy = vi.fn();
1011
+ provider.on("chainChanged", chainChangedSpy);
1012
+
1013
+ (provider as any)._handleChainChanged({ chainId: "0x1", isConnected: true });
1014
+
1015
+ expect(chainChangedSpy).not.toHaveBeenCalled();
1016
+ });
1017
+
1018
+ it("should handle invalid chainId", () => {
1019
+ const chainChangedSpy = vi.fn();
1020
+ provider.on("chainChanged", chainChangedSpy);
1021
+
1022
+ (provider as any)._handleChainChanged({ chainId: "invalid", isConnected: true });
1023
+
1024
+ expect(chainChangedSpy).not.toHaveBeenCalled();
1025
+ });
1026
+
1027
+ it("should handle disconnect when isConnected is false", () => {
1028
+ // First set provider as connected
1029
+ provider.emit("connect", { chainId: "0x1" });
1030
+ expect(provider.isConnected()).toBe(true);
1031
+
1032
+ const disconnectSpy = vi.fn();
1033
+ provider.on("disconnect", disconnectSpy);
1034
+
1035
+ (provider as any)._handleChainChanged({ chainId: "0x1", isConnected: false });
1036
+
1037
+ expect(disconnectSpy).toHaveBeenCalled();
1038
+ });
1039
+ });
1040
+
1041
+ describe("_handleAccountsChanged", () => {
1042
+ beforeEach(() => {
1043
+ provider = new EvmProvider(productInfo, {
1044
+ sendRequest: mockSendRequest,
1045
+ onResponse: mockOnResponse,
1046
+ });
1047
+ });
1048
+
1049
+ it("should handle valid accounts change", async () => {
1050
+ await new Promise((resolve) => setTimeout(resolve, 100));
1051
+
1052
+ const accountsChangedSpy = vi.fn();
1053
+ provider.on("accountsChanged", accountsChangedSpy);
1054
+
1055
+ (provider as any)._handleAccountsChanged(["0x123", "0x456"]);
1056
+
1057
+ expect(provider.selectedAddress).toBe("0x123");
1058
+ expect(accountsChangedSpy).toHaveBeenCalledWith(["0x123", "0x456"]);
1059
+ });
1060
+
1061
+ it("should handle empty accounts", async () => {
1062
+ await new Promise((resolve) => setTimeout(resolve, 100));
1063
+
1064
+ provider.selectedAddress = "0x123";
1065
+ const accountsChangedSpy = vi.fn();
1066
+ provider.on("accountsChanged", accountsChangedSpy);
1067
+
1068
+ (provider as any)._handleAccountsChanged([]);
1069
+
1070
+ expect(provider.selectedAddress).toBeNull();
1071
+ expect(accountsChangedSpy).toHaveBeenCalledWith([]);
1072
+ });
1073
+
1074
+ it("should handle invalid accounts parameter", () => {
1075
+ const errorSpy = vi.spyOn((provider as any)._log, "error");
1076
+
1077
+ (provider as any)._handleAccountsChanged("invalid" as any);
1078
+
1079
+ expect(errorSpy).toHaveBeenCalled();
1080
+ });
1081
+
1082
+ it("should handle non-string account", () => {
1083
+ const errorSpy = vi.spyOn((provider as any)._log, "error");
1084
+
1085
+ (provider as any)._handleAccountsChanged([123 as any, "0x123"]);
1086
+
1087
+ expect(errorSpy).toHaveBeenCalled();
1088
+ });
1089
+
1090
+ it("should not emit if accounts haven't changed", async () => {
1091
+ await new Promise((resolve) => setTimeout(resolve, 100));
1092
+
1093
+ // Set accounts using the same array reference to ensure comparison works
1094
+ const accounts = ["0x123"];
1095
+ (provider as any)._state.accounts = accounts;
1096
+ const accountsChangedSpy = vi.fn();
1097
+ provider.on("accountsChanged", accountsChangedSpy);
1098
+
1099
+ // Use the same array reference
1100
+ (provider as any)._handleAccountsChanged(accounts);
1101
+
1102
+ expect(accountsChangedSpy).not.toHaveBeenCalled();
1103
+ });
1104
+ });
1105
+
1106
+ describe("_handleUnlockStateChanged", () => {
1107
+ beforeEach(() => {
1108
+ provider = new EvmProvider(productInfo, {
1109
+ sendRequest: mockSendRequest,
1110
+ onResponse: mockOnResponse,
1111
+ });
1112
+ });
1113
+
1114
+ it("should handle unlock state change", () => {
1115
+ const accountsChangedSpy = vi.fn();
1116
+ provider.on("accountsChanged", accountsChangedSpy);
1117
+
1118
+ (provider as any)._handleUnlockStateChanged({
1119
+ accounts: ["0x123"],
1120
+ isUnlocked: true,
1121
+ });
1122
+
1123
+ expect((provider as any)._state.isUnlocked).toBe(true);
1124
+ expect(accountsChangedSpy).toHaveBeenCalled();
1125
+ });
1126
+
1127
+ it("should handle invalid isUnlocked parameter", () => {
1128
+ const errorSpy = vi.spyOn((provider as any)._log, "error");
1129
+
1130
+ (provider as any)._handleUnlockStateChanged({
1131
+ isUnlocked: "invalid" as any,
1132
+ });
1133
+
1134
+ expect(errorSpy).toHaveBeenCalled();
1135
+ });
1136
+
1137
+ it("should not change if isUnlocked is the same", () => {
1138
+ (provider as any)._state.isUnlocked = false;
1139
+ const accountsChangedSpy = vi.fn();
1140
+ provider.on("accountsChanged", accountsChangedSpy);
1141
+
1142
+ (provider as any)._handleUnlockStateChanged({
1143
+ isUnlocked: false,
1144
+ });
1145
+
1146
+ // Should not call accountsChanged if unlock state didn't change
1147
+ expect((provider as any)._state.isUnlocked).toBe(false);
1148
+ });
1149
+ });
1150
+
1151
+ describe("send", () => {
1152
+ beforeEach(() => {
1153
+ provider = new EvmProvider(productInfo, {
1154
+ sendRequest: mockSendRequest,
1155
+ onResponse: mockOnResponse,
1156
+ logger: {
1157
+ log: vi.fn(),
1158
+ warn: vi.fn(),
1159
+ error: vi.fn(),
1160
+ debug: vi.fn(),
1161
+ info: vi.fn(),
1162
+ trace: vi.fn(),
1163
+ },
1164
+ });
1165
+ });
1166
+
1167
+ it("should warn on first send call", () => {
1168
+ const warnSpy = vi.spyOn((provider as any)._log, "warn");
1169
+
1170
+ const mockHandle = vi.fn();
1171
+ (provider as any)._rpcEngine = {
1172
+ handle: mockHandle,
1173
+ };
1174
+
1175
+ provider.send("eth_getBalance", ["0x123"]);
1176
+
1177
+ expect(warnSpy).toHaveBeenCalled();
1178
+ });
1179
+
1180
+ it("should handle string method with params", async () => {
1181
+ const mockHandle = vi.fn((payload, callback) => {
1182
+ callback(null, { result: "0x1000" });
1183
+ });
1184
+ (provider as any)._rpcEngine = {
1185
+ handle: mockHandle,
1186
+ };
1187
+
1188
+ const result = await provider.send("eth_getBalance", ["0x123"]);
1189
+
1190
+ expect(mockHandle).toHaveBeenCalled();
1191
+ expect(result).toBeDefined();
1192
+ });
1193
+
1194
+ it("should handle payload object with callback", () => {
1195
+ const callback = vi.fn();
1196
+ const payload = {
1197
+ jsonrpc: "2.0" as const,
1198
+ id: 1,
1199
+ method: "eth_getBalance",
1200
+ params: ["0x123"],
1201
+ };
1202
+
1203
+ const mockHandle = vi.fn();
1204
+ (provider as any)._rpcEngine = {
1205
+ handle: mockHandle,
1206
+ };
1207
+
1208
+ provider.send(payload, callback);
1209
+
1210
+ expect(mockHandle).toHaveBeenCalled();
1211
+ });
1212
+
1213
+ it("should handle sync methods", () => {
1214
+ provider.selectedAddress = "0x123";
1215
+
1216
+ const result = provider.send({
1217
+ jsonrpc: "2.0" as const,
1218
+ id: 1,
1219
+ method: "eth_accounts",
1220
+ } as any) as any;
1221
+
1222
+ expect(result.result).toEqual(["0x123"]);
1223
+ });
1224
+
1225
+ it("should handle eth_coinbase sync", () => {
1226
+ provider.selectedAddress = "0x123";
1227
+
1228
+ const result = provider.send({
1229
+ jsonrpc: "2.0" as const,
1230
+ id: 1,
1231
+ method: "eth_coinbase",
1232
+ } as any) as any;
1233
+
1234
+ expect(result.result).toBe("0x123");
1235
+ });
1236
+
1237
+ it("should handle eth_uninstallFilter sync", () => {
1238
+ const mockHandle = vi.fn();
1239
+ (provider as any)._rpcEngine = {
1240
+ handle: mockHandle,
1241
+ };
1242
+
1243
+ const result = provider.send({
1244
+ jsonrpc: "2.0" as const,
1245
+ id: 1,
1246
+ method: "eth_uninstallFilter",
1247
+ params: ["0x123"],
1248
+ } as any) as any;
1249
+
1250
+ expect(result.result).toBe(true);
1251
+ expect(mockHandle).toHaveBeenCalled();
1252
+ });
1253
+
1254
+ it("should handle net_version sync", () => {
1255
+ provider.networkVersion = "1";
1256
+
1257
+ const result = provider.send({
1258
+ jsonrpc: "2.0" as const,
1259
+ id: 1,
1260
+ method: "net_version",
1261
+ } as any) as any;
1262
+
1263
+ expect(result.result).toBe("1");
1264
+ });
1265
+
1266
+ it("should throw error for unsupported sync method", () => {
1267
+ expect(() => {
1268
+ provider.send({
1269
+ jsonrpc: "2.0",
1270
+ id: 1,
1271
+ method: "unsupported_method",
1272
+ } as any);
1273
+ }).toThrow();
1274
+ });
1275
+ });
1276
+
1277
+ describe("enable", () => {
1278
+ beforeEach(() => {
1279
+ provider = new EvmProvider(productInfo, {
1280
+ sendRequest: mockSendRequest,
1281
+ onResponse: mockOnResponse,
1282
+ logger: {
1283
+ log: vi.fn(),
1284
+ warn: vi.fn(),
1285
+ error: vi.fn(),
1286
+ debug: vi.fn(),
1287
+ info: vi.fn(),
1288
+ trace: vi.fn(),
1289
+ },
1290
+ });
1291
+ });
1292
+
1293
+ it("should call connect", async () => {
1294
+ const connectSpy = vi.spyOn(provider, "connect").mockResolvedValue(["0x123"]);
1295
+
1296
+ await provider.enable();
1297
+
1298
+ expect(connectSpy).toHaveBeenCalled();
1299
+ });
1300
+ });
1301
+
1302
+ describe("setConnectedStatus", () => {
1303
+ beforeEach(() => {
1304
+ provider = new EvmProvider(productInfo, {
1305
+ sendRequest: mockSendRequest,
1306
+ onResponse: mockOnResponse,
1307
+ });
1308
+ });
1309
+
1310
+ it("should set connected status and accounts", () => {
1311
+ provider.setConnectedStatus({
1312
+ connected: true,
1313
+ address: ["0x123", "0x456"],
1314
+ });
1315
+
1316
+ expect((provider as any)._state.isConnected).toBe(true);
1317
+ expect((provider as any)._state.accounts).toEqual(["0x123", "0x456"]);
1318
+ });
1319
+ });
1320
+
1321
+ describe("initializeEvmProvider", () => {
1322
+ it("should create provider with Proxy", () => {
1323
+ const provider = initializeEvmProvider(productInfo, {
1324
+ sendRequest: mockSendRequest,
1325
+ onResponse: mockOnResponse,
1326
+ shouldSetOnWindow: false,
1327
+ });
1328
+
1329
+ expect(provider).toBeDefined();
1330
+ // Test Proxy deleteProperty
1331
+ delete (provider as any).testProperty;
1332
+ expect(true).toBe(true); // Proxy should allow deletion
1333
+ });
1334
+
1335
+ it("should set provider on window when shouldSetOnWindow is true", () => {
1336
+ initializeEvmProvider(productInfo, {
1337
+ sendRequest: mockSendRequest,
1338
+ onResponse: mockOnResponse,
1339
+ shouldSetOnWindow: true,
1340
+ });
1341
+
1342
+ expect((window as any).ethereum).toBeDefined();
1343
+ });
1344
+ });
1345
+
1346
+ describe("setGlobalProvider", () => {
1347
+ it("should set provider on window and dispatch event", () => {
1348
+ const provider = new EvmProvider(productInfo, {
1349
+ sendRequest: mockSendRequest,
1350
+ onResponse: mockOnResponse,
1351
+ });
1352
+
1353
+ const eventSpy = vi.fn();
1354
+ window.addEventListener("ethereum#initialized", eventSpy);
1355
+
1356
+ setGlobalProvider(provider);
1357
+
1358
+ expect((window as any).ethereum).toBe(provider);
1359
+ expect(eventSpy).toHaveBeenCalled();
1360
+ });
1361
+ });
1362
+ });