@tomo-inc/wallet-adaptor-base 0.0.20 → 0.0.22

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,537 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { WalletConnectSolanaProvider } from "../wallets/providers/WalletConnectSolanaProvider";
3
+ import { WalletConnectClient } from "@tomo-inc/wallet-connect-protocol";
4
+
5
+ // Mock WalletConnectClient - use vi.hoisted to create mock instance and constructor
6
+ const { mockClient, MockWalletConnectClient } = vi.hoisted(() => {
7
+ const createMockClient = () => ({
8
+ initialize: vi.fn().mockResolvedValue(undefined),
9
+ connect: vi.fn().mockResolvedValue("wc:test-uri"),
10
+ disconnectSession: vi.fn().mockResolvedValue(undefined),
11
+ getActiveSessions: vi.fn().mockReturnValue([]),
12
+ sendRequest: vi.fn().mockResolvedValue("result"),
13
+ on: vi.fn(),
14
+ off: vi.fn(),
15
+ });
16
+
17
+ const mockClientInstance = createMockClient();
18
+
19
+ // Create a proper constructor function
20
+ function MockWalletConnectClientConstructor() {
21
+ return mockClientInstance;
22
+ }
23
+
24
+ // Wrap with vi.fn() to make it spyable
25
+ const MockClient = vi.fn(MockWalletConnectClientConstructor);
26
+
27
+ return {
28
+ mockClient: mockClientInstance,
29
+ MockWalletConnectClient: MockClient,
30
+ };
31
+ });
32
+
33
+ vi.mock("@tomo-inc/wallet-connect-protocol", () => ({
34
+ WalletConnectClient: MockWalletConnectClient,
35
+ }));
36
+
37
+ describe("WalletConnectSolanaProvider", () => {
38
+ let provider: WalletConnectSolanaProvider;
39
+ const config = {
40
+ projectId: "test-project-id",
41
+ metadata: {
42
+ name: "Test App",
43
+ description: "Test Description",
44
+ url: "https://test.com",
45
+ icons: ["https://test.com/icon.png"],
46
+ },
47
+ };
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ provider = new WalletConnectSolanaProvider(config);
52
+ });
53
+
54
+ afterEach(() => {
55
+ vi.restoreAllMocks();
56
+ });
57
+
58
+ describe("constructor and client listeners", () => {
59
+ it("should create provider with default Solana namespaces", () => {
60
+ expect(WalletConnectClient).toHaveBeenCalledWith({
61
+ projectId: config.projectId,
62
+ metadata: config.metadata,
63
+ });
64
+ expect(mockClient.on).toHaveBeenCalledWith("session_proposal", expect.any(Function));
65
+ expect(mockClient.on).toHaveBeenCalledWith("session_delete", expect.any(Function));
66
+ expect(mockClient.on).toHaveBeenCalledWith("session_update", expect.any(Function));
67
+ expect(mockClient.on).toHaveBeenCalledWith("display_uri", expect.any(Function));
68
+ });
69
+
70
+ it("should handle session_proposal and update session when getActiveSessions returns sessions", () => {
71
+ const mockSession = {
72
+ topic: "proposal-topic",
73
+ namespaces: {
74
+ solana: { accounts: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:SolanaKey456"] },
75
+ },
76
+ };
77
+ mockClient.getActiveSessions.mockReturnValue([mockSession]);
78
+ const sessionProposalHandler = (mockClient.on as any).mock.calls.find((c: any[]) => c[0] === "session_proposal")?.[1];
79
+ sessionProposalHandler();
80
+ expect(provider["session"]).toEqual(mockSession);
81
+ expect(provider["accounts"]).toEqual([{ publicKey: "SolanaKey456" }]);
82
+ expect(provider["chainId"]).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
83
+ });
84
+
85
+ it("should handle session_delete and clear session", () => {
86
+ provider["session"] = { topic: "t", namespaces: {} } as any;
87
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
88
+ const sessionDeleteHandler = (mockClient.on as any).mock.calls.find((c: any[]) => c[0] === "session_delete")?.[1];
89
+ sessionDeleteHandler();
90
+ expect(provider["session"]).toBeNull();
91
+ expect(provider["accounts"]).toEqual([]);
92
+ });
93
+
94
+ it("should handle session_update and emit accountsChanged", () => {
95
+ provider["session"] = {
96
+ topic: "t",
97
+ namespaces: { solana: { accounts: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:SolanaKey789"] } },
98
+ } as any;
99
+ const sessionUpdateHandler = (mockClient.on as any).mock.calls.find((c: any[]) => c[0] === "session_update")?.[1];
100
+ provider["updateAccountsFromSession"] = vi.fn();
101
+ sessionUpdateHandler();
102
+ expect(provider["updateAccountsFromSession"]).toHaveBeenCalled();
103
+ });
104
+
105
+ it("should handle display_uri and set uri", () => {
106
+ const displayUriHandler = (mockClient.on as any).mock.calls.find((c: any[]) => c[0] === "display_uri")?.[1];
107
+ displayUriHandler({ uri: "wc://solana-uri" });
108
+ expect(provider.uri).toBe("wc://solana-uri");
109
+ });
110
+
111
+ it("should use custom namespaces when provided", () => {
112
+ const customNamespaces = {
113
+ solana: {
114
+ chains: ["solana:devnet"],
115
+ methods: ["solana_getAccounts"],
116
+ events: [],
117
+ },
118
+ };
119
+
120
+ const customProvider = new WalletConnectSolanaProvider({
121
+ ...config,
122
+ namespaces: customNamespaces,
123
+ });
124
+
125
+ expect(customProvider).toBeDefined();
126
+ });
127
+ });
128
+
129
+ describe("initialize", () => {
130
+ it("should initialize client", async () => {
131
+ await provider.initialize();
132
+
133
+ expect(mockClient.initialize).toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ describe("getUri", () => {
138
+ it("should return existing URI if available", async () => {
139
+ provider.uri = "wc:existing-uri";
140
+ const uri = await provider.getUri();
141
+
142
+ expect(uri).toBe("wc:existing-uri");
143
+ expect(mockClient.connect).not.toHaveBeenCalled();
144
+ });
145
+
146
+ it("should create new URI if not available", async () => {
147
+ mockClient.connect.mockResolvedValue("wc:new-uri");
148
+ const uri = await provider.getUri();
149
+
150
+ expect(uri).toBe("wc:new-uri");
151
+ expect(mockClient.connect).toHaveBeenCalled();
152
+ expect(provider.uri).toBe("wc:new-uri");
153
+ });
154
+ });
155
+
156
+ describe("connect", () => {
157
+ it("should return existing accounts if available", async () => {
158
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
159
+
160
+ const result = await provider.connect();
161
+
162
+ expect(result).toEqual({ publicKey: "SolanaKey123" });
163
+ });
164
+
165
+ it("should connect and wait for session", async () => {
166
+ vi.useFakeTimers();
167
+ mockClient.connect.mockResolvedValue("wc:test-uri");
168
+ mockClient.getActiveSessions.mockReturnValueOnce([]).mockReturnValueOnce([
169
+ {
170
+ topic: "test-topic",
171
+ namespaces: {
172
+ solana: {
173
+ accounts: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:SolanaKey123"],
174
+ },
175
+ },
176
+ },
177
+ ]);
178
+
179
+ const connectPromise = provider.connect();
180
+ setTimeout(() => {
181
+ const sessions = mockClient.getActiveSessions();
182
+ if (sessions.length > 0) {
183
+ const handlers = (mockClient.on as any).mock.calls.filter((call: any[]) => call[0] === "session_proposal");
184
+ if (handlers.length > 0) {
185
+ handlers[0][1]();
186
+ }
187
+ }
188
+ }, 100);
189
+
190
+ vi.advanceTimersByTime(200);
191
+ const result = await connectPromise;
192
+
193
+ expect(result).toBeDefined();
194
+ vi.useRealTimers();
195
+ });
196
+
197
+ it("should return undefined and emit error when waitForConnection times out", async () => {
198
+ vi.useFakeTimers();
199
+ mockClient.getActiveSessions.mockReturnValue([]);
200
+ mockClient.connect.mockResolvedValue("wc:uri");
201
+ const connectPromise = provider.connect();
202
+ await vi.advanceTimersByTimeAsync(300001);
203
+ const result = await connectPromise;
204
+ expect(result).toBeUndefined();
205
+ vi.useRealTimers();
206
+ });
207
+ });
208
+
209
+ describe("disconnect", () => {
210
+ it("should disconnect session", async () => {
211
+ provider["session"] = {
212
+ topic: "test-topic",
213
+ namespaces: {},
214
+ } as any;
215
+
216
+ await provider.disconnect();
217
+
218
+ expect(mockClient.disconnectSession).toHaveBeenCalledWith("test-topic");
219
+ expect(provider["session"]).toBeNull();
220
+ expect(provider["accounts"]).toEqual([]);
221
+ });
222
+
223
+ it("should handle disconnect when not connected", async () => {
224
+ provider["session"] = null;
225
+
226
+ await provider.disconnect();
227
+
228
+ expect(mockClient.disconnectSession).not.toHaveBeenCalled();
229
+ });
230
+ });
231
+
232
+ describe("request", () => {
233
+ it("should handle solana_requestAccounts when connected", async () => {
234
+ provider["session"] = {
235
+ topic: "test-topic",
236
+ namespaces: {},
237
+ } as any;
238
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
239
+
240
+ const result = await provider.request({ method: "solana_requestAccounts" });
241
+
242
+ expect(result).toEqual({ publicKey: "SolanaKey123" });
243
+ });
244
+
245
+ it("should call connect when solana_requestAccounts and not connected", async () => {
246
+ vi.useFakeTimers();
247
+ provider["session"] = null;
248
+ provider["accounts"] = [];
249
+ mockClient.connect.mockResolvedValue("wc:uri");
250
+ mockClient.getActiveSessions.mockReturnValue([]);
251
+ const requestPromise = provider.request({ method: "solana_requestAccounts" });
252
+ expect(mockClient.connect).toHaveBeenCalled();
253
+ await vi.advanceTimersByTimeAsync(300001);
254
+ const result = await requestPromise;
255
+ expect(result).toBeUndefined();
256
+ vi.useRealTimers();
257
+ });
258
+
259
+ it("should handle solana_getAccounts", async () => {
260
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
261
+
262
+ const result = await provider.request({ method: "solana_getAccounts" });
263
+
264
+ expect(result).toEqual([{ publicKey: "SolanaKey123" }]);
265
+ });
266
+
267
+ it("should send request to client", async () => {
268
+ provider["session"] = {
269
+ topic: "test-topic",
270
+ namespaces: {},
271
+ } as any;
272
+ provider["chainId"] = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
273
+
274
+ await provider.request({
275
+ method: "solana_signMessage",
276
+ params: { message: "test", pubkey: "SolanaKey123" },
277
+ });
278
+
279
+ expect(mockClient.sendRequest).toHaveBeenCalledWith({
280
+ topic: "test-topic",
281
+ chainId: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
282
+ request: {
283
+ method: "solana_signMessage",
284
+ params: { message: "test", pubkey: "SolanaKey123" },
285
+ },
286
+ });
287
+ });
288
+
289
+ it("should throw error when not connected", async () => {
290
+ provider["session"] = null;
291
+
292
+ await expect(
293
+ provider.request({
294
+ method: "solana_signMessage",
295
+ params: [],
296
+ }),
297
+ ).rejects.toThrow("Not connected");
298
+ });
299
+
300
+ it("should handle request errors", async () => {
301
+ provider["session"] = {
302
+ topic: "test-topic",
303
+ namespaces: {},
304
+ } as any;
305
+ mockClient.sendRequest.mockRejectedValue(new Error("Request failed"));
306
+
307
+ await expect(
308
+ provider.request({
309
+ method: "solana_signMessage",
310
+ params: [],
311
+ }),
312
+ ).rejects.toThrow("Request failed");
313
+ });
314
+ });
315
+
316
+ describe("getAccounts", () => {
317
+ it("should return accounts when connected", async () => {
318
+ provider["session"] = {
319
+ topic: "test-topic",
320
+ namespaces: {},
321
+ } as any;
322
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
323
+
324
+ const accounts = await provider.getAccounts();
325
+
326
+ expect(accounts).toEqual([{ publicKey: "SolanaKey123" }]);
327
+ });
328
+
329
+ it("should return empty array when not connected", async () => {
330
+ provider["session"] = null;
331
+
332
+ const accounts = await provider.getAccounts();
333
+
334
+ expect(accounts).toEqual([{ publicKey: "" }]);
335
+ });
336
+ });
337
+
338
+ describe("getChainId", () => {
339
+ it("should return current chain ID", async () => {
340
+ provider["chainId"] = "solana:devnet";
341
+ const chainId = await provider.getChainId();
342
+
343
+ expect(chainId).toBe("solana:devnet");
344
+ });
345
+ });
346
+
347
+ describe("isConnected", () => {
348
+ it("should return true when connected", () => {
349
+ provider["session"] = {
350
+ topic: "test-topic",
351
+ namespaces: {},
352
+ } as any;
353
+ provider["accounts"] = [{ publicKey: "SolanaKey123" }];
354
+
355
+ expect(provider.isConnected()).toBe(true);
356
+ });
357
+
358
+ it("should return false when not connected", () => {
359
+ provider["session"] = null;
360
+ expect(provider.isConnected()).toBe(false);
361
+ });
362
+
363
+ it("should return false when no accounts", () => {
364
+ provider["session"] = {
365
+ topic: "test-topic",
366
+ namespaces: {},
367
+ } as any;
368
+ provider["accounts"] = [];
369
+
370
+ expect(provider.isConnected()).toBe(false);
371
+ });
372
+ });
373
+
374
+ describe("getSession", () => {
375
+ it("should return current session", () => {
376
+ const mockSession = {
377
+ topic: "test-topic",
378
+ namespaces: {},
379
+ } as any;
380
+ provider["session"] = mockSession;
381
+
382
+ expect(provider.getSession()).toBe(mockSession);
383
+ });
384
+
385
+ it("should return null when not connected", () => {
386
+ provider["session"] = null;
387
+
388
+ expect(provider.getSession()).toBeNull();
389
+ });
390
+ });
391
+
392
+ describe("event listeners", () => {
393
+ it("should add event listener", () => {
394
+ const listener = vi.fn();
395
+ provider.on("test-event", listener);
396
+
397
+ expect(provider["eventListeners"].has("test-event")).toBe(true);
398
+ });
399
+
400
+ it("should remove event listener", () => {
401
+ const listener = vi.fn();
402
+ provider.on("test-event", listener);
403
+ provider.off("test-event", listener);
404
+
405
+ expect(provider["eventListeners"].get("test-event")?.has(listener)).toBe(false);
406
+ });
407
+
408
+ it("should emit events to listeners", () => {
409
+ const listener = vi.fn();
410
+ provider.on("test-event", listener);
411
+
412
+ provider["emit"]("test-event", { data: "test" });
413
+
414
+ expect(listener).toHaveBeenCalledWith({ data: "test" });
415
+ });
416
+
417
+ it("should handle listener errors gracefully", () => {
418
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
419
+ const listener = vi.fn(() => {
420
+ throw new Error("Listener error");
421
+ });
422
+ provider.on("test-event", listener);
423
+
424
+ provider["emit"]("test-event", { data: "test" });
425
+
426
+ expect(consoleErrorSpy).toHaveBeenCalled();
427
+ consoleErrorSpy.mockRestore();
428
+ });
429
+ });
430
+
431
+ describe("helper methods", () => {
432
+ beforeEach(() => {
433
+ provider["session"] = {
434
+ topic: "test-topic",
435
+ namespaces: {},
436
+ } as any;
437
+ provider["chainId"] = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
438
+ });
439
+
440
+ it("should sign message", async () => {
441
+ mockClient.sendRequest.mockResolvedValue({ signature: "0xsignature" });
442
+
443
+ const signature = await provider.signMessage("test message", "SolanaKey123");
444
+
445
+ expect(signature).toBe("0xsignature");
446
+ expect(mockClient.sendRequest).toHaveBeenCalledWith({
447
+ topic: "test-topic",
448
+ chainId: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
449
+ request: {
450
+ method: "solana_signMessage",
451
+ params: {
452
+ message: "test message",
453
+ pubkey: "SolanaKey123",
454
+ },
455
+ },
456
+ });
457
+ });
458
+
459
+ it("should sign transaction", async () => {
460
+ mockClient.sendRequest.mockResolvedValue({ signature: "0xsignature", transaction: "0xtx" });
461
+
462
+ const result = await provider.signTransaction("0xtransaction");
463
+
464
+ expect(result).toEqual({ signature: "0xsignature", transaction: "0xtx" });
465
+ });
466
+
467
+ it("should sign all transactions", async () => {
468
+ mockClient.sendRequest.mockResolvedValue({ transactions: ["0xtx1", "0xtx2"] });
469
+
470
+ const result = await provider.signAllTransactions(["0xtx1", "0xtx2"]);
471
+
472
+ expect(result).toEqual({ transactions: ["0xtx1", "0xtx2"] });
473
+ });
474
+
475
+ it("should sign and send transaction", async () => {
476
+ mockClient.sendRequest.mockResolvedValue({ signature: "0xsignature" });
477
+
478
+ const result = await provider.signAndSendTransaction("0xtransaction", { skipPreflight: true });
479
+
480
+ expect(result).toEqual({ signature: "0xsignature" });
481
+ });
482
+ });
483
+
484
+ describe("updateAccountsFromSession", () => {
485
+ it("should update accounts from session", () => {
486
+ const mockSession = {
487
+ topic: "test-topic",
488
+ namespaces: {
489
+ solana: {
490
+ accounts: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:SolanaKey123"],
491
+ },
492
+ },
493
+ } as any;
494
+
495
+ provider["session"] = mockSession;
496
+ provider["updateAccountsFromSession"]();
497
+
498
+ expect(provider["accounts"]).toEqual([{ publicKey: "SolanaKey123" }]);
499
+ expect(provider["chainId"]).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
500
+ });
501
+
502
+ it("should filter only Solana accounts", () => {
503
+ const mockSession = {
504
+ topic: "test-topic",
505
+ namespaces: {
506
+ solana: {
507
+ accounts: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:SolanaKey123"],
508
+ },
509
+ eip155: {
510
+ accounts: ["eip155:1:0x123"],
511
+ },
512
+ },
513
+ } as any;
514
+
515
+ provider["session"] = mockSession;
516
+ provider["updateAccountsFromSession"]();
517
+
518
+ expect(provider["accounts"]).toEqual([{ publicKey: "SolanaKey123" }]);
519
+ });
520
+
521
+ it("should handle empty accounts", () => {
522
+ const mockSession = {
523
+ topic: "test-topic",
524
+ namespaces: {
525
+ solana: {
526
+ accounts: [],
527
+ },
528
+ },
529
+ } as any;
530
+
531
+ provider["session"] = mockSession;
532
+ provider["updateAccountsFromSession"]();
533
+
534
+ expect(provider["accounts"]).toEqual([]);
535
+ });
536
+ });
537
+ });