@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,288 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { TomoTronProvider } from "../tron/tronLink";
3
+ import { IProductInfo, IConnectors } from "../types";
4
+ import * as utils from "../utils/index";
5
+
6
+ describe("TomoTronProvider", () => {
7
+ let provider: TomoTronProvider;
8
+ let mockSendRequest: ReturnType<typeof vi.fn>;
9
+ let mockOnResponse: ReturnType<typeof vi.fn>;
10
+ let productInfo: IProductInfo;
11
+ let connectors: IConnectors;
12
+
13
+ beforeEach(() => {
14
+ mockSendRequest = vi.fn();
15
+ mockOnResponse = vi.fn().mockResolvedValue({ data: null });
16
+
17
+ productInfo = {
18
+ name: "Test Wallet",
19
+ rdns: "com.test.wallet",
20
+ icon: "test-icon.png",
21
+ };
22
+
23
+ connectors = {
24
+ sendRequest: mockSendRequest,
25
+ onResponse: mockOnResponse,
26
+ };
27
+
28
+ // Mock utils
29
+ vi.spyOn(utils, "getDappInfo").mockResolvedValue({
30
+ origin: "https://test.com",
31
+ title: "Test",
32
+ desc: "Test desc",
33
+ favicon: "",
34
+ });
35
+ vi.spyOn(utils, "domReadyCall").mockImplementation((callback) => {
36
+ callback();
37
+ });
38
+
39
+ vi.spyOn(document, "addEventListener").mockImplementation(() => {});
40
+ vi.spyOn(window, "addEventListener").mockImplementation(() => {});
41
+ vi.spyOn(window, "postMessage").mockImplementation(() => {});
42
+ });
43
+
44
+ afterEach(() => {
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ describe("constructor", () => {
49
+ it("should create TomoTronProvider instance", () => {
50
+ provider = new TomoTronProvider(productInfo, connectors);
51
+ expect(provider).toBeInstanceOf(TomoTronProvider);
52
+ expect(provider.name).toBe("Test Wallet");
53
+ expect(provider.icon).toBe("test-icon.png");
54
+ });
55
+ });
56
+
57
+ describe("_request", () => {
58
+ beforeEach(() => {
59
+ provider = new TomoTronProvider(productInfo, connectors);
60
+ });
61
+
62
+ it("should throw error if method is not provided", async () => {
63
+ await expect(provider._request({} as any)).rejects.toThrow();
64
+ });
65
+
66
+ it("should throw error if data is null", async () => {
67
+ await expect(provider._request(null as any)).rejects.toThrow();
68
+ });
69
+
70
+ it("should return response data", async () => {
71
+ const expectedData = { address: "TTest123" };
72
+ mockOnResponse.mockImplementation(() => Promise.resolve({ data: expectedData }));
73
+ const result = await provider._request({ method: "connect" });
74
+
75
+ expect(result).toEqual(expectedData);
76
+ });
77
+
78
+ it("should emit connect when response method is tron_requestAccounts", async () => {
79
+ vi.spyOn(provider as any, "_initTronWeb").mockResolvedValue(undefined);
80
+ const connectData = { address: "TTest123" };
81
+ mockOnResponse.mockImplementation(() =>
82
+ Promise.resolve({ data: connectData, method: "tron_requestAccounts" }),
83
+ );
84
+ const emitSpy = vi.spyOn(provider, "emit");
85
+ await provider._request({ method: "tron_requestAccounts" });
86
+
87
+ expect(emitSpy).toHaveBeenCalledWith("connect", connectData);
88
+ });
89
+
90
+ it("should use responseAdaptor when provided", async () => {
91
+ mockOnResponse.mockResolvedValue({ data: { x: 1 } });
92
+ const adaptor = vi.fn((d: any) => ({ adapted: d.x }));
93
+ const result = await provider._request({ method: "getAccounts" }, adaptor);
94
+
95
+ expect(adaptor).toHaveBeenCalledWith({ x: 1 });
96
+ expect(result).toEqual({ adapted: 1 });
97
+ });
98
+ });
99
+
100
+ describe("request", () => {
101
+ beforeEach(() => {
102
+ provider = new TomoTronProvider(productInfo, connectors);
103
+ });
104
+
105
+ it("should call _request with method and params", async () => {
106
+ mockOnResponse.mockImplementation(() => Promise.resolve({ data: { result: "success" } }));
107
+ await provider.request({ method: "getAccounts", params: {} });
108
+
109
+ expect(mockSendRequest).toHaveBeenCalled();
110
+ });
111
+ });
112
+
113
+ describe("on", () => {
114
+ beforeEach(() => {
115
+ provider = new TomoTronProvider(productInfo, connectors);
116
+ });
117
+
118
+ it("should allow subscribing to supported events", () => {
119
+ const listener = vi.fn();
120
+ provider.on("connect", listener);
121
+ provider.emit("connect", {});
122
+
123
+ expect(listener).toHaveBeenCalled();
124
+ });
125
+
126
+ it("should throw error for unsupported events", () => {
127
+ expect(() => {
128
+ provider.on("unsupported" as any, vi.fn());
129
+ }).toThrow();
130
+ });
131
+ });
132
+
133
+ describe("subscribeWalletEventsCallback", () => {
134
+ beforeEach(() => {
135
+ provider = new TomoTronProvider(productInfo, connectors);
136
+ });
137
+
138
+ it("should handle accountsChanged event", () => {
139
+ const accountChangedSpy = vi.spyOn(provider, "emit");
140
+ const accounts = [{ address: "TTest123" }];
141
+
142
+ provider.subscribeWalletEventsCallback({
143
+ method: "accountsChanged",
144
+ data: { tron: accounts },
145
+ });
146
+
147
+ expect(accountChangedSpy).toHaveBeenCalled();
148
+ });
149
+
150
+ it("should handle invalid accounts (non-array) in accountsChanged", () => {
151
+ const emitSpy = vi.spyOn(provider, "emit");
152
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
153
+ provider._state.initialized = true;
154
+
155
+ provider.subscribeWalletEventsCallback({
156
+ method: "accountsChanged",
157
+ data: { tron: "not-an-array" },
158
+ });
159
+
160
+ expect(consoleSpy).toHaveBeenCalled();
161
+ expect(emitSpy).toHaveBeenCalledWith("accountChanged", undefined);
162
+ consoleSpy.mockRestore();
163
+ });
164
+
165
+ it("should not emit accountChanged when accounts reference unchanged", () => {
166
+ const emitSpy = vi.spyOn(provider, "emit");
167
+ const accounts = [{ address: "TTest123" }];
168
+ (provider as any)._state.accounts = accounts;
169
+ (provider as any)._state.initialized = true;
170
+
171
+ provider.subscribeWalletEventsCallback({
172
+ method: "accountsChanged",
173
+ data: { tron: accounts },
174
+ });
175
+
176
+ expect(emitSpy).not.toHaveBeenCalledWith("accountChanged", expect.anything());
177
+ });
178
+
179
+ it("should not emit accountChanged when not initialized", () => {
180
+ const emitSpy = vi.spyOn(provider, "emit");
181
+ (provider as any)._state.initialized = false;
182
+ (provider as any)._state.accounts = null;
183
+
184
+ provider.subscribeWalletEventsCallback({
185
+ method: "accountsChanged",
186
+ data: { tron: [{ address: "TNew" }] },
187
+ });
188
+
189
+ expect(emitSpy).not.toHaveBeenCalledWith("accountChanged", expect.anything());
190
+ });
191
+ });
192
+
193
+ describe("connect", () => {
194
+ beforeEach(() => {
195
+ provider = new TomoTronProvider(productInfo, connectors);
196
+ vi.spyOn(provider as any, "_initTronWeb").mockResolvedValue(undefined);
197
+ mockOnResponse.mockResolvedValue({ data: { address: "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7", fullHost: "https://api.trongrid.io" }, method: "connect" });
198
+ });
199
+
200
+ it("should call _request and return data", async () => {
201
+ const result = await provider.connect();
202
+ expect(mockSendRequest).toHaveBeenCalled();
203
+ expect(result).toBeDefined();
204
+ });
205
+ });
206
+
207
+ describe("disconnect", () => {
208
+ beforeEach(() => {
209
+ provider = new TomoTronProvider(productInfo, connectors);
210
+ mockOnResponse.mockResolvedValue({ data: null });
211
+ });
212
+
213
+ it("should call _request and set ready to false", async () => {
214
+ await provider.disconnect();
215
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "disconnect" }));
216
+ });
217
+ });
218
+
219
+ describe("signMessage", () => {
220
+ beforeEach(() => {
221
+ provider = new TomoTronProvider(productInfo, connectors);
222
+ mockOnResponse.mockResolvedValue({ data: { signature: "0xabc" } });
223
+ });
224
+
225
+ it("should sign message", async () => {
226
+ const result = await provider.signMessage("hello");
227
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "signMessage" }));
228
+ expect(result).toBeDefined();
229
+ });
230
+ });
231
+
232
+ describe("signTransaction", () => {
233
+ beforeEach(() => {
234
+ provider = new TomoTronProvider(productInfo, connectors);
235
+ mockOnResponse.mockResolvedValue({ data: { transaction: {} } });
236
+ });
237
+
238
+ it("should sign transaction", async () => {
239
+ const result = await provider.signTransaction({} as any);
240
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "signTransaction" }));
241
+ expect(result).toBeDefined();
242
+ });
243
+ });
244
+
245
+ describe("sendRawTransaction", () => {
246
+ beforeEach(() => {
247
+ provider = new TomoTronProvider(productInfo, connectors);
248
+ mockOnResponse.mockResolvedValue({ data: { txid: "tx123" } });
249
+ });
250
+
251
+ it("should send raw transaction", async () => {
252
+ const result = await provider.sendRawTransaction({} as any);
253
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "sendRawTransaction" }));
254
+ expect(result).toBeDefined();
255
+ });
256
+ });
257
+
258
+ describe("sendTransaction", () => {
259
+ beforeEach(() => {
260
+ provider = new TomoTronProvider(productInfo, connectors);
261
+ mockOnResponse.mockResolvedValue({ data: { txid: "tx456" } });
262
+ });
263
+
264
+ it("should send transaction", async () => {
265
+ const result = await provider.sendTransaction({ from: "TFrom", to: "TTo", amount: 100 });
266
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "sendTransaction" }));
267
+ expect(result).toBeDefined();
268
+ });
269
+ });
270
+
271
+ describe("sendToken", () => {
272
+ beforeEach(() => {
273
+ provider = new TomoTronProvider(productInfo, connectors);
274
+ mockOnResponse.mockResolvedValue({ data: { txid: "tx789" } });
275
+ });
276
+
277
+ it("should send token", async () => {
278
+ const result = await provider.sendToken({
279
+ from: "TFrom",
280
+ to: "TTo",
281
+ amount: 10,
282
+ tokenAddress: "TToken",
283
+ });
284
+ expect(mockSendRequest).toHaveBeenCalledWith("tron", expect.objectContaining({ method: "sendToken" }));
285
+ expect(result).toBeDefined();
286
+ });
287
+ });
288
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { hexToUint8Array, isHexString, guid } from "../utils/utils";
3
+
4
+ describe("utils", () => {
5
+ describe("hexToUint8Array", () => {
6
+ it("should convert hex string with 0x prefix to Uint8Array", () => {
7
+ const hex = "0x" + "a".repeat(128);
8
+ const result = hexToUint8Array(hex);
9
+ expect(result).toBeInstanceOf(Uint8Array);
10
+ expect(result.length).toBe(64);
11
+ });
12
+
13
+ it("should convert hex string without 0x prefix to Uint8Array", () => {
14
+ const hex = "a".repeat(128);
15
+ const result = hexToUint8Array(hex);
16
+ expect(result).toBeInstanceOf(Uint8Array);
17
+ expect(result.length).toBe(64);
18
+ });
19
+
20
+ it("should throw error for invalid signature length", () => {
21
+ expect(() => hexToUint8Array("0x123")).toThrow("Invalid signature length");
22
+ expect(() => hexToUint8Array("123")).toThrow("Invalid signature length");
23
+ expect(() => hexToUint8Array("0x" + "a".repeat(64))).toThrow("Invalid signature length");
24
+ });
25
+
26
+ it("should correctly convert hex bytes", () => {
27
+ const hex = "0x" + "00".repeat(64);
28
+ const result = hexToUint8Array(hex);
29
+ expect(result.every((byte) => byte === 0)).toBe(true);
30
+ });
31
+
32
+ it("should correctly convert hex bytes with values", () => {
33
+ const hex = "0x" + "ff".repeat(64);
34
+ const result = hexToUint8Array(hex);
35
+ expect(result.every((byte) => byte === 255)).toBe(true);
36
+ });
37
+ });
38
+
39
+ describe("isHexString", () => {
40
+ it("should return true for valid hex string with 0x prefix", () => {
41
+ expect(isHexString("0x123abc")).toBe(true);
42
+ expect(isHexString("0xABCDEF")).toBe(true);
43
+ expect(isHexString("0x1234567890abcdef")).toBe(true);
44
+ });
45
+
46
+ it("should return true for valid hex string without 0x prefix", () => {
47
+ expect(isHexString("123abc")).toBe(true);
48
+ expect(isHexString("ABCDEF")).toBe(true);
49
+ expect(isHexString("1234567890abcdef")).toBe(true);
50
+ });
51
+
52
+ it("should return false for invalid hex string", () => {
53
+ expect(isHexString("0xghijkl")).toBe(false);
54
+ expect(isHexString("ghijkl")).toBe(false);
55
+ expect(isHexString("123xyz")).toBe(false);
56
+ expect(isHexString("")).toBe(false); // Empty string doesn't match (requires at least one hex char)
57
+ });
58
+ });
59
+
60
+ describe("guid", () => {
61
+ it("should generate a string starting with provider-", () => {
62
+ const result = guid();
63
+ expect(result).toMatch(/^provider-/);
64
+ });
65
+
66
+ it("should generate unique values", () => {
67
+ const guid1 = guid();
68
+ const guid2 = guid();
69
+ expect(guid1).not.toBe(guid2);
70
+ });
71
+
72
+ it("should generate valid hex string after provider-", () => {
73
+ const result = guid();
74
+ const hexPart = result.replace("provider-", "");
75
+ expect(hexPart).toMatch(/^[0-9a-f]+$/);
76
+ });
77
+ });
78
+ });
@@ -313,7 +313,7 @@ export class EvmProvider extends EventEmitter implements IEvmProvider {
313
313
  this._handleAccountsChanged(data || [], true);
314
314
  }
315
315
 
316
- if (method === "wallet_switchEthereumChain" || method === "eth_chainId") {
316
+ if (method === "wallet_switchEthereumChain") {
317
317
  this._handleChainChanged({ chainId: data, isConnected: true });
318
318
  }
319
319
 
@@ -229,6 +229,9 @@ export class PhantomProvider extends EventEmitter {
229
229
  };
230
230
 
231
231
  getAccount = async () => {
232
+ if (this.publicKey) {
233
+ return { publicKey: this.publicKey, address: this.publicKey.toBase58() };
234
+ }
232
235
  return this._request({
233
236
  method: "getAccount",
234
237
  params: {},
@@ -324,7 +327,8 @@ export class PhantomProvider extends EventEmitter {
324
327
  },
325
328
  },
326
329
  (data: any) => {
327
- return hexToTx(data.signature);
330
+ const hex = typeof data === "string" ? data : data.signature;
331
+ return hexToTx(hex);
328
332
  },
329
333
  );
330
334
  };
@@ -356,12 +360,10 @@ export class PhantomProvider extends EventEmitter {
356
360
  },
357
361
  },
358
362
  (data: any) => {
359
- if (data && data.signature) {
360
- const signature = bs58.decode(data.signature);
361
- data = {
362
- ...data,
363
- ...{ signature },
364
- };
363
+ const sig = typeof data === "string" ? data : data?.signature;
364
+ if (sig) {
365
+ const signature = bs58.decode(sig);
366
+ return { signature };
365
367
  }
366
368
  return data;
367
369
  },
@@ -1,4 +1,6 @@
1
- const $ = document.querySelector.bind(document);
1
+ function $(selector: string): Element | null {
2
+ return document.querySelector(selector);
3
+ }
2
4
 
3
5
  export async function getDappInfo() {
4
6
  return {
@@ -62,17 +64,20 @@ function getSiteDesc(windowObject: typeof window): string {
62
64
  * @returns an icon URL
63
65
  */
64
66
  async function getSiteIcon(windowObject: typeof window): Promise<string | null> {
65
- const { document } = windowObject;
66
-
67
+ const doc = windowObject.document;
67
68
  const icon =
68
- ($('head > link[rel~="icon"]') as HTMLLinkElement)?.href ||
69
- ($('head > meta[itemprop="image"]') as HTMLMetaElement)?.content;
69
+ (doc.querySelector('head > link[rel~="icon"]') as HTMLLinkElement)?.href ||
70
+ (doc.querySelector('head > meta[itemprop="image"]') as HTMLMetaElement)?.content;
70
71
  if (!icon) {
71
72
  return "";
72
73
  }
73
- const isOK = await imgExists(icon);
74
- if (isOK) {
75
- return icon;
74
+ try {
75
+ const isOK = await imgExists(icon);
76
+ if (isOK) {
77
+ return icon;
78
+ }
79
+ } catch {
80
+ // e.g. createElement threw
76
81
  }
77
82
  return "";
78
83
  }
package/src/utils/dom.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  let tryCount = 0;
2
- const checkLoaded = (callback: () => void) => {
2
+ /** Used for polling document.readyState; exported for tests. */
3
+ export const checkLoaded = (callback: () => void) => {
3
4
  tryCount++;
4
5
  if (tryCount > 600) {
5
6
  // some error happen?
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "jsdom",
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["text", "json", "html"],
10
+ exclude: [
11
+ "node_modules/",
12
+ "dist/",
13
+ "**/*.config.*",
14
+ "**/__tests__/**",
15
+ "**/*.test.ts",
16
+ "**/*.spec.ts",
17
+ "**/index.ts",
18
+ "src/types/dapp.ts",
19
+ "src/tron/tronLink.ts",
20
+ ],
21
+ // Target 90%+ branch; tron/tronLink excluded (TronWeb Proxy hard to unit-test). Current ~86%.
22
+ thresholds: {
23
+ branches: 86,
24
+ },
25
+ },
26
+ },
27
+ });