@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,375 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { PhantomProvider } from "../solana/phantom";
3
+ import { IProductInfo, IConnectors } from "../types";
4
+ import { PublicKey, Transaction } from "@solana/web3.js";
5
+ import * as utils from "../utils/index";
6
+ import * as solanaUtils from "../solana/utils";
7
+
8
+ describe("PhantomProvider", () => {
9
+ let provider: PhantomProvider;
10
+ let mockSendRequest: ReturnType<typeof vi.fn>;
11
+ let mockOnResponse: ReturnType<typeof vi.fn>;
12
+ let productInfo: IProductInfo;
13
+ let connectors: IConnectors;
14
+
15
+ beforeEach(() => {
16
+ mockSendRequest = vi.fn();
17
+ mockOnResponse = vi.fn().mockResolvedValue({ data: null });
18
+
19
+ productInfo = {
20
+ name: "Test Wallet",
21
+ rdns: "com.test.wallet",
22
+ icon: "test-icon.png",
23
+ };
24
+
25
+ connectors = {
26
+ sendRequest: mockSendRequest,
27
+ onResponse: mockOnResponse,
28
+ };
29
+
30
+ // Mock utils
31
+ vi.spyOn(utils, "getDappInfo").mockResolvedValue({
32
+ origin: "https://test.com",
33
+ title: "Test",
34
+ desc: "Test desc",
35
+ favicon: "",
36
+ });
37
+ vi.spyOn(utils, "domReadyCall").mockImplementation((callback) => {
38
+ callback();
39
+ });
40
+
41
+ vi.spyOn(document, "addEventListener").mockImplementation(() => {});
42
+ vi.spyOn(window, "addEventListener").mockImplementation(() => {});
43
+ vi.useFakeTimers();
44
+ });
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ vi.useRealTimers();
49
+ });
50
+
51
+ describe("constructor", () => {
52
+ it("should create PhantomProvider instance", () => {
53
+ provider = new PhantomProvider(productInfo, connectors);
54
+ expect(provider).toBeInstanceOf(PhantomProvider);
55
+ expect(provider.name).toBe("Test Wallet");
56
+ expect(provider.icon).toBe("test-icon.png");
57
+ });
58
+
59
+ it("should handle window message subscribeWalletEvents", () => {
60
+ let messageHandler: ((e: MessageEvent) => void) | null = null;
61
+ vi.mocked(window.addEventListener).mockImplementation((event: string, handler: any) => {
62
+ if (event === "message") messageHandler = handler;
63
+ });
64
+ provider = new PhantomProvider(productInfo, connectors);
65
+ provider._state.initialized = true;
66
+ const emitSpy = vi.spyOn(provider, "emit");
67
+ if (messageHandler) {
68
+ messageHandler({
69
+ data: {
70
+ type: "subscribeWalletEvents",
71
+ method: "accountsChanged",
72
+ data: { solana: [{ publicKey: "11111111111111111111111111111111" }] },
73
+ },
74
+ } as MessageEvent);
75
+ }
76
+ expect(emitSpy).toHaveBeenCalledWith("accountChanged", expect.any(PublicKey));
77
+ });
78
+
79
+ it("should call _request on visibilitychange when visible", async () => {
80
+ let visibilityHandler: (() => void) | null = null;
81
+ vi.mocked(document.addEventListener).mockImplementation((event: string, handler: any) => {
82
+ if (event === "visibilitychange") visibilityHandler = handler;
83
+ });
84
+ provider = new PhantomProvider(productInfo, connectors);
85
+ mockOnResponse.mockResolvedValue({});
86
+ Object.defineProperty(document, "visibilityState", {
87
+ value: "visible",
88
+ writable: true,
89
+ configurable: true,
90
+ });
91
+ if (visibilityHandler) await visibilityHandler();
92
+ expect(mockSendRequest).toHaveBeenCalledWith(
93
+ "solana",
94
+ expect.objectContaining({ method: "wallet_sendDomainMetadata" }),
95
+ );
96
+ });
97
+
98
+ });
99
+
100
+ describe("_request", () => {
101
+ beforeEach(() => {
102
+ provider = new PhantomProvider(productInfo, connectors);
103
+ });
104
+
105
+ it("should throw error if method is not provided", async () => {
106
+ await expect(provider._request({} as any)).rejects.toThrow();
107
+ });
108
+
109
+ it("should return response data", async () => {
110
+ const expectedData = { publicKey: "11111111111111111111111111111111" };
111
+ mockOnResponse.mockImplementation(() => Promise.resolve({ data: expectedData }));
112
+ const result = await provider._request({ method: "connect" });
113
+
114
+ expect(result).toBeDefined();
115
+ });
116
+
117
+ it("should convert publicKey to PublicKey object", async () => {
118
+ const publicKey = "11111111111111111111111111111111";
119
+ mockOnResponse.mockImplementation(() => Promise.resolve({ data: { publicKey } }));
120
+ const result: any = await provider._request({ method: "connect" });
121
+
122
+ expect(result?.publicKey).toBeInstanceOf(PublicKey);
123
+ });
124
+ });
125
+
126
+ describe("request", () => {
127
+ beforeEach(() => {
128
+ provider = new PhantomProvider(productInfo, connectors);
129
+ });
130
+
131
+ it("should handle signMessage method", async () => {
132
+ const message = new Uint8Array([1, 2, 3]);
133
+ mockOnResponse.mockImplementation(() => Promise.resolve({ data: { signature: "0x123" } }));
134
+ await provider.request({ method: "signMessage", params: { message } }, undefined);
135
+
136
+ expect(mockSendRequest).toHaveBeenCalled();
137
+ });
138
+ });
139
+
140
+ describe("on", () => {
141
+ beforeEach(() => {
142
+ provider = new PhantomProvider(productInfo, connectors);
143
+ });
144
+
145
+ it("should allow subscribing to supported events", () => {
146
+ const listener = vi.fn();
147
+ provider.on("connect", listener);
148
+ provider.emit("connect");
149
+
150
+ expect(listener).toHaveBeenCalled();
151
+ });
152
+
153
+ it("should throw error for unsupported events", () => {
154
+ expect(() => {
155
+ provider.on("unsupported" as any, vi.fn());
156
+ }).toThrow();
157
+ });
158
+ });
159
+
160
+ describe("subscribeWalletEventsCallback", () => {
161
+ beforeEach(() => {
162
+ provider = new PhantomProvider(productInfo, connectors);
163
+ provider._state.initialized = true;
164
+ });
165
+
166
+ it("should handle accountsChanged event", () => {
167
+ const accountChangedSpy = vi.spyOn(provider, "emit");
168
+ const accounts = [{ publicKey: "11111111111111111111111111111111" }];
169
+
170
+ provider.subscribeWalletEventsCallback({
171
+ method: "accountsChanged",
172
+ data: { sol: accounts },
173
+ });
174
+
175
+ expect(accountChangedSpy).toHaveBeenCalled();
176
+ });
177
+ });
178
+
179
+ describe("connect", () => {
180
+ beforeEach(() => {
181
+ provider = new PhantomProvider(productInfo, connectors);
182
+ mockOnResponse.mockResolvedValue({ data: { publicKey: "11111111111111111111111111111111" } });
183
+ });
184
+
185
+ it("should call _request and emit connect", async () => {
186
+ const result = await provider.connect();
187
+ expect(mockSendRequest).toHaveBeenCalled();
188
+ expect(result?.publicKey).toBeInstanceOf(PublicKey);
189
+ });
190
+ });
191
+
192
+ describe("disconnect", () => {
193
+ beforeEach(() => {
194
+ provider = new PhantomProvider(productInfo, connectors);
195
+ mockOnResponse.mockResolvedValue({ data: null });
196
+ });
197
+
198
+ it("should call _request and clear publicKey", async () => {
199
+ await provider.disconnect();
200
+ expect(mockSendRequest).toHaveBeenCalled();
201
+ });
202
+ });
203
+
204
+ describe("getAccount", () => {
205
+ beforeEach(() => {
206
+ provider = new PhantomProvider(productInfo, connectors);
207
+ mockOnResponse.mockResolvedValue({ data: { address: "11111111111111111111111111111111" } });
208
+ });
209
+
210
+ it("should request getAccount", async () => {
211
+ const result = await provider.getAccount();
212
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "getAccount" }));
213
+ expect(result).toBeDefined();
214
+ });
215
+ });
216
+
217
+ describe("signMessage (direct)", () => {
218
+ const validSigHex = "0x" + "00".repeat(64);
219
+
220
+ beforeEach(() => {
221
+ provider = new PhantomProvider(productInfo, connectors);
222
+ mockOnResponse.mockResolvedValue({ data: { signature: validSigHex, signedMessage: "hello" } });
223
+ });
224
+
225
+ it("should sign message and adapt response", async () => {
226
+ const result = await provider.signMessage(new Uint8Array([1, 2, 3]), "utf8");
227
+ expect(mockSendRequest).toHaveBeenCalled();
228
+ expect(result).toBeDefined();
229
+ });
230
+ });
231
+
232
+ describe("signIn", () => {
233
+ const validSigHex = "0x" + "00".repeat(64);
234
+
235
+ beforeEach(() => {
236
+ provider = new PhantomProvider(productInfo, connectors);
237
+ mockOnResponse.mockResolvedValue({ data: { signature: validSigHex, signedMessage: "msg" } });
238
+ });
239
+
240
+ it("should call signIn with params", async () => {
241
+ const result = await provider.signIn({ statement: "Sign in", nonce: "n1" });
242
+ expect(mockSendRequest).toHaveBeenCalled();
243
+ expect(result).toBeDefined();
244
+ });
245
+
246
+ it("should include version, issuedAt, resources in signIn message", async () => {
247
+ await provider.signIn({
248
+ statement: "Hi",
249
+ version: "1",
250
+ nonce: "n",
251
+ issuedAt: "2024-01-01",
252
+ resources: ["https://example.com" as `https://${string}`],
253
+ });
254
+ expect(mockSendRequest).toHaveBeenCalled();
255
+ const call = mockSendRequest.mock.calls.find((c: any[]) => c[1]?.params?.message?.includes("Version:1"));
256
+ expect(call).toBeDefined();
257
+ expect(call[1].params.message).toContain("Issued At:2024-01-01");
258
+ expect(call[1].params.message).toContain("Resources:");
259
+ });
260
+ });
261
+
262
+ describe("signTransaction", () => {
263
+ beforeEach(() => {
264
+ vi.spyOn(solanaUtils, "txToHex").mockReturnValue("deadbeef");
265
+ vi.spyOn(solanaUtils, "hexToTx").mockReturnValue(new Transaction());
266
+ provider = new PhantomProvider(productInfo, connectors);
267
+ mockOnResponse.mockResolvedValue({ data: { signature: "00".repeat(64) } });
268
+ });
269
+
270
+ it("should sign transaction", async () => {
271
+ const result = await provider.signTransaction(new Transaction());
272
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "signTransaction" }));
273
+ expect(result).toBeDefined();
274
+ });
275
+ });
276
+
277
+ describe("signAllTransactions", () => {
278
+ beforeEach(() => {
279
+ vi.spyOn(solanaUtils, "txToHex").mockReturnValue("deadbeef");
280
+ provider = new PhantomProvider(productInfo, connectors);
281
+ mockOnResponse.mockResolvedValue({ data: ["sig1", "sig2"] });
282
+ });
283
+
284
+ it("should sign multiple transactions", async () => {
285
+ const result = await provider.signAllTransactions([new Transaction(), new Transaction()]);
286
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "signAllTransactions" }));
287
+ expect(result).toBeDefined();
288
+ });
289
+ });
290
+
291
+ describe("signAndSendTransaction", () => {
292
+ beforeEach(() => {
293
+ vi.spyOn(solanaUtils, "txToHex").mockReturnValue("deadbeef");
294
+ provider = new PhantomProvider(productInfo, connectors);
295
+ mockOnResponse.mockResolvedValue({ data: { signature: "base58sig" } });
296
+ });
297
+
298
+ it("should sign and send transaction", async () => {
299
+ const result = await provider.signAndSendTransaction(new Transaction());
300
+ expect(mockSendRequest).toHaveBeenCalled();
301
+ expect(result).toBeDefined();
302
+ });
303
+ });
304
+
305
+ describe("signAndSendAllTransactions", () => {
306
+ beforeEach(() => {
307
+ vi.spyOn(solanaUtils, "txToHex").mockReturnValue("deadbeef");
308
+ provider = new PhantomProvider(productInfo, connectors);
309
+ mockOnResponse.mockResolvedValue({ data: null });
310
+ });
311
+
312
+ it("should sign and send all transactions", async () => {
313
+ const result = await provider.signAndSendAllTransactions([new Transaction()]);
314
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "signAndSendAllTransactions" }));
315
+ expect(result).toBeDefined();
316
+ });
317
+ });
318
+
319
+ describe("sendSolana", () => {
320
+ beforeEach(() => {
321
+ provider = new PhantomProvider(productInfo, connectors);
322
+ mockOnResponse.mockResolvedValue({ data: { signature: "hexsig" } });
323
+ });
324
+
325
+ it("should send solana tx", async () => {
326
+ const result = await provider.sendSolana({ from: "addr1", to: "addr2", amount: 1 });
327
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "sendSolana" }));
328
+ expect(result).toBeDefined();
329
+ });
330
+
331
+ it("should handle base58 signature in sendSolana response", async () => {
332
+ const base58Sig = "2NEpo7TZRRrLZSi2U";
333
+ mockOnResponse.mockResolvedValue({ data: { signature: base58Sig } });
334
+ const result = await provider.sendSolana({ from: "addr1", to: "addr2", amount: 1 });
335
+ expect(result).toBeDefined();
336
+ expect((result as any)?.signature).toBeDefined();
337
+ });
338
+ });
339
+
340
+ describe("sendToken", () => {
341
+ beforeEach(() => {
342
+ provider = new PhantomProvider(productInfo, connectors);
343
+ mockOnResponse.mockResolvedValue({ data: { signature: "hexsig" } });
344
+ });
345
+
346
+ it("should send token tx", async () => {
347
+ const result = await provider.sendToken({ from: "addr1", to: "addr2", amount: 1, tokenAddress: "mint" });
348
+ expect(mockSendRequest).toHaveBeenCalledWith("solana", expect.objectContaining({ method: "sendToken" }));
349
+ expect(result).toBeDefined();
350
+ });
351
+
352
+ it("should handle hex signature in sendToken response", async () => {
353
+ const hexSig = "0x" + "00".repeat(64);
354
+ mockOnResponse.mockResolvedValue({ data: { signature: hexSig } });
355
+ const result = await provider.sendToken({ from: "addr1", to: "addr2", amount: 1, tokenAddress: "mint" });
356
+ expect(result).toBeDefined();
357
+ expect((result as any)?.signature).toBeInstanceOf(Uint8Array);
358
+ });
359
+ });
360
+
361
+ describe("_handleAccountsChanged branches", () => {
362
+ beforeEach(() => {
363
+ provider = new PhantomProvider(productInfo, connectors);
364
+ provider._state.initialized = true;
365
+ });
366
+
367
+ it("should not set publicKey when first account has no publicKey or address", () => {
368
+ provider.subscribeWalletEventsCallback({
369
+ method: "accountsChanged",
370
+ data: { solana: [{}] },
371
+ });
372
+ expect(provider.publicKey).toBeNull();
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Transaction, VersionedTransaction } from "@solana/web3.js";
3
+ import { txToHex, hexToTx } from "../solana/utils";
4
+
5
+ describe("solana/utils", () => {
6
+ describe("txToHex", () => {
7
+ it("should return string as-is if input is already a string", () => {
8
+ const hexString = "0x123456";
9
+ const result = txToHex(hexString as any);
10
+ expect(result).toBe(hexString);
11
+ });
12
+
13
+ it("should serialize VersionedTransaction to hex", () => {
14
+ // Create a minimal VersionedTransaction-like object
15
+ const mockTx = {
16
+ message: {},
17
+ signatures: [new Uint8Array(64)],
18
+ serialize: () => Buffer.from("test-transaction-data"),
19
+ } as unknown as VersionedTransaction;
20
+
21
+ const result = txToHex(mockTx);
22
+ expect(result).toBe("746573742d7472616e73616374696f6e2d64617461");
23
+ });
24
+
25
+ it("should serialize Transaction to hex", () => {
26
+ const mockTx = {
27
+ serialize: (options?: { requireAllSignatures?: boolean; verifySignatures?: boolean }) => {
28
+ return Buffer.from("test-transaction-data");
29
+ },
30
+ } as unknown as Transaction;
31
+
32
+ const result = txToHex(mockTx);
33
+ expect(result).toBe("746573742d7472616e73616374696f6e2d64617461");
34
+ });
35
+
36
+ it("should serialize Transaction with options", () => {
37
+ const mockTx = {
38
+ serialize: (options?: { requireAllSignatures?: boolean; verifySignatures?: boolean }) => {
39
+ expect(options?.requireAllSignatures).toBe(false);
40
+ expect(options?.verifySignatures).toBe(false);
41
+ return Buffer.from("test-transaction-data");
42
+ },
43
+ } as unknown as Transaction;
44
+
45
+ const result = txToHex(mockTx);
46
+ expect(result).toBe("746573742d7472616e73616374696f6e2d64617461");
47
+ });
48
+ });
49
+
50
+ describe("hexToTx", () => {
51
+ it("should throw error for invalid hex string", () => {
52
+ const invalidHex = "invalid-hex-string";
53
+ expect(() => hexToTx(invalidHex)).toThrow("Failed to deserialize transaction");
54
+ });
55
+
56
+ it("should handle empty hex string", () => {
57
+ expect(() => hexToTx("")).toThrow();
58
+ });
59
+ });
60
+ });