@tomo-inc/chains-service 0.0.23 → 0.0.25

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.
Files changed (51) hide show
  1. package/dist/index.cjs +42 -24
  2. package/dist/index.d.cts +3 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +43 -25
  5. package/package.json +3 -2
  6. package/project.json +1 -1
  7. package/src/__tests__/config.test.ts +46 -0
  8. package/src/__tests__/dogecoin-utils.test.ts +147 -0
  9. package/src/__tests__/evm-utils.test.ts +133 -0
  10. package/src/__tests__/index.test.ts +40 -0
  11. package/src/__tests__/services.test.ts +285 -0
  12. package/src/__tests__/solana-utils.test.ts +131 -0
  13. package/src/__tests__/utils.test.ts +52 -0
  14. package/src/__tests__/wallet.test.ts +350 -0
  15. package/src/api/__tests__/base.test.ts +146 -0
  16. package/src/api/__tests__/index.test.ts +51 -0
  17. package/src/api/__tests__/network.test.ts +153 -0
  18. package/src/api/__tests__/token.test.ts +231 -2
  19. package/src/api/__tests__/transaction.test.ts +121 -6
  20. package/src/api/__tests__/user.test.ts +237 -3
  21. package/src/api/__tests__/wallet.test.ts +174 -4
  22. package/src/api/network.ts +9 -1
  23. package/src/api/utils/__tests__/index.test.ts +91 -0
  24. package/src/api/utils/__tests__/signature.test.ts +124 -0
  25. package/src/api/utils/index.ts +6 -2
  26. package/src/base/__tests__/network.test.ts +119 -0
  27. package/src/base/__tests__/service.test.ts +68 -0
  28. package/src/base/__tests__/token.test.ts +123 -0
  29. package/src/base/__tests__/transaction.test.ts +210 -0
  30. package/src/config.ts +2 -1
  31. package/src/dogecoin/__tests__/base.test.ts +76 -0
  32. package/src/dogecoin/__tests__/rpc.test.ts +465 -0
  33. package/src/dogecoin/__tests__/service-extended.test.ts +420 -0
  34. package/src/dogecoin/__tests__/utils-doge.test.ts +244 -0
  35. package/src/dogecoin/__tests__/utils-extended.test.ts +323 -0
  36. package/src/dogecoin/base.ts +1 -0
  37. package/src/dogecoin/config.ts +2 -2
  38. package/src/dogecoin/rpc.ts +10 -1
  39. package/src/dogecoin/service.ts +9 -5
  40. package/src/evm/__tests__/rpc.test.ts +132 -0
  41. package/src/evm/__tests__/service.test.ts +535 -0
  42. package/src/evm/__tests__/utils.test.ts +170 -0
  43. package/src/evm/service.ts +11 -0
  44. package/src/evm/utils.ts +2 -2
  45. package/src/solana/__tests__/service.test.ts +425 -0
  46. package/src/solana/__tests__/utils.test.ts +937 -0
  47. package/src/solana/config.ts +1 -1
  48. package/src/solana/service.ts +2 -0
  49. package/src/solana/utils.ts +2 -16
  50. package/src/utils/index.ts +1 -1
  51. package/vitest.config.ts +13 -0
@@ -203,6 +203,17 @@ export class EvmService extends BaseService {
203
203
  };
204
204
  }
205
205
 
206
+ public async request(method: string, params: any): Promise<any> {
207
+ try {
208
+ const rpcClient = await this.createPublicClient({});
209
+ const res = await rpcClient.request(method, params);
210
+ return res;
211
+ } catch (error) {
212
+ console.error("Failed to request:", error);
213
+ throw error;
214
+ }
215
+ }
216
+
206
217
  private async createPublicClient({ chainId, rpcUrl }: { chainId?: string; rpcUrl?: string }): Promise<any> {
207
218
  if (rpcUrl) {
208
219
  return createPublicClient({
package/src/evm/utils.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  import { CenterSubmitParams, SubmitParamsType } from "../types";
14
14
 
15
15
  export const isEvmChain = (network: any) => {
16
- return network && network?.platformType === "EVM";
16
+ return network?.platformType === "EVM" || false;
17
17
  };
18
18
 
19
19
  export const isHexChainId = (chainIdHex: string) => {
@@ -36,7 +36,7 @@ export const getAllTypeChainIds = ({ chainId, chainType }: { chainId: string | n
36
36
  }
37
37
  const chainIdHex = toHex(Number(chainId));
38
38
  return {
39
- chainId,
39
+ chainId: String(chainId),
40
40
  chainIdHex,
41
41
  chainUid: `${chainType}:${chainId}`,
42
42
  };
@@ -0,0 +1,425 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { SolanaService } from "../service";
3
+ import { ChainTypeEnum } from "@tomo-inc/wallet-utils";
4
+ import { IAccountInfo, TomoAppInfo } from "../../types";
5
+
6
+ // Capture Connection constructor config for coverage of custom fetch (line 31)
7
+ let lastConnectionConfig: { fetch?: (url: string, options: any) => Promise<any> } | undefined;
8
+
9
+ // Mock Solana Connection
10
+ vi.mock("@solana/web3.js", () => {
11
+ const mockSimulateTransaction = vi.fn().mockResolvedValue({
12
+ value: { err: null, logs: [] },
13
+ });
14
+ const mockSendAndConfirmRawTransaction = vi.fn().mockResolvedValue("signature123");
15
+ const mockGetSignatureStatus = vi.fn().mockResolvedValue({
16
+ value: { confirmationStatus: "confirmed" },
17
+ });
18
+ const mockGetAccountInfo = vi.fn().mockResolvedValue({ lamports: 1000000 });
19
+ const mockGetLatestBlockhash = vi.fn().mockResolvedValue({
20
+ blockhash: "blockhash123",
21
+ lastValidBlockHeight: 100,
22
+ });
23
+
24
+ class MockConnection {
25
+ simulateTransaction = mockSimulateTransaction;
26
+ getAccountInfo = mockGetAccountInfo;
27
+ getLatestBlockhash = mockGetLatestBlockhash;
28
+ constructor(
29
+ public rpcUrl: string,
30
+ public config?: any,
31
+ ) {
32
+ lastConnectionConfig = config;
33
+ }
34
+ }
35
+
36
+ return {
37
+ Connection: MockConnection,
38
+ PublicKey: class {
39
+ constructor(public address: string) {}
40
+ toString() {
41
+ return this.address;
42
+ }
43
+ },
44
+ VersionedTransaction: {
45
+ deserialize: vi.fn().mockReturnValue({
46
+ signatures: [{ toString: () => "signature123" }],
47
+ }),
48
+ },
49
+ sendAndConfirmRawTransaction: mockSendAndConfirmRawTransaction,
50
+ };
51
+ });
52
+
53
+ // Mock @solana/spl-token
54
+ vi.mock("@solana/spl-token", () => ({
55
+ getAssociatedTokenAddress: vi.fn().mockResolvedValue("tokenAddress123"),
56
+ }));
57
+
58
+ // Mock solana utils for _queryGasInfo
59
+ vi.mock("../utils", () => ({
60
+ createLegacyTx: vi.fn().mockResolvedValue({ serialize: () => Buffer.from("tx") }),
61
+ createTokenLegacyTransaction2: vi.fn().mockResolvedValue({ serialize: () => Buffer.from("tx") }),
62
+ txToHex: vi.fn().mockReturnValue("deadbeef"),
63
+ }));
64
+
65
+ describe("SolanaService", () => {
66
+ let mockAccountInfo: IAccountInfo;
67
+ let mockTomoAppInfo: TomoAppInfo;
68
+ let service: SolanaService;
69
+
70
+ beforeEach(() => {
71
+ vi.clearAllMocks();
72
+ (SolanaService as any).instance = undefined;
73
+
74
+ mockAccountInfo = {
75
+ getCurrent: vi.fn().mockResolvedValue({
76
+ address: "SolanaAddress123",
77
+ publicKey: "test-public-key",
78
+ }),
79
+ signMessage: vi.fn().mockResolvedValue("signature123"),
80
+ signTransaction: vi.fn().mockResolvedValue("signedTx123"),
81
+ } as any;
82
+
83
+ mockTomoAppInfo = {
84
+ tomoStage: "dev",
85
+ appId: "test-app",
86
+ appName: "Test App",
87
+ };
88
+
89
+ service = new SolanaService(ChainTypeEnum.SOLANA, mockAccountInfo, mockTomoAppInfo);
90
+ });
91
+
92
+ describe("constructor and getInstance", () => {
93
+ it("should pass custom fetch in Connection config and cover fetch call (line 31)", async () => {
94
+ expect(lastConnectionConfig).toBeDefined();
95
+ expect(lastConnectionConfig!.fetch).toBeDefined();
96
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true });
97
+ vi.stubGlobal("fetch", mockFetch);
98
+ const fetchResult = lastConnectionConfig!.fetch!("https://example.com/rpc", { method: "POST" });
99
+ expect(fetchResult).toBeDefined();
100
+ expect(typeof fetchResult.then).toBe("function");
101
+ await fetchResult;
102
+ expect(mockFetch).toHaveBeenCalledWith("https://example.com/rpc", { method: "POST" });
103
+ });
104
+
105
+ it("should return same instance from getInstance", () => {
106
+ const first = SolanaService.getInstance(ChainTypeEnum.SOLANA, mockAccountInfo, mockTomoAppInfo);
107
+ const second = SolanaService.getInstance(ChainTypeEnum.SOLANA, mockAccountInfo, mockTomoAppInfo);
108
+ expect(first).toBe(second);
109
+ });
110
+ });
111
+
112
+ describe("getAccount", () => {
113
+ it("should get account with publicKey", async () => {
114
+ const account = await service.getAccount();
115
+ expect(account.publicKey).toBe("test-public-key");
116
+ expect(account.address).toBe("SolanaAddress123");
117
+ });
118
+
119
+ it("should use address as publicKey when publicKey is missing", async () => {
120
+ mockAccountInfo.getCurrent = vi.fn().mockResolvedValue({
121
+ address: "SolanaAddress123",
122
+ });
123
+ const account = await service.getAccount();
124
+ expect(account.publicKey).toBe("SolanaAddress123");
125
+ });
126
+ });
127
+
128
+ describe("signMessage", () => {
129
+ it("should sign message with prefix", async () => {
130
+ const result = await service.signMessage({ message: "test message" });
131
+ expect(result.signature).toBe("signature123");
132
+ expect(result.message).toContain("test message");
133
+ expect(mockAccountInfo.signMessage).toHaveBeenCalledWith("test message");
134
+ });
135
+ });
136
+
137
+ describe("signIn", () => {
138
+ it("should sign in successfully", async () => {
139
+ const result = await service.signIn({ message: "test message" });
140
+ expect(result.signature).toBe("signature123");
141
+ expect(result.address).toBe("SolanaAddress123");
142
+ expect(result.signedMessage).toContain("test message");
143
+ });
144
+ });
145
+
146
+ describe("request", () => {
147
+ it("should call method via request", async () => {
148
+ const result = await service.request({ method: "getAccount", params: {} });
149
+ expect(result).toBeDefined();
150
+ });
151
+
152
+ it("should throw error for unsupported method", async () => {
153
+ await expect(service.request({ method: "unsupportedMethod", params: {} })).rejects.toThrow(
154
+ "unsupportedMethod in request is not supported",
155
+ );
156
+ });
157
+ });
158
+
159
+ describe("signTransaction", () => {
160
+ it("should sign transaction", async () => {
161
+ const result = await service.signTransaction({ rawTransaction: "rawTx123" });
162
+ expect(result).toBe("signedTx123");
163
+ expect(mockAccountInfo.signTransaction).toHaveBeenCalled();
164
+ });
165
+ });
166
+
167
+ describe("signAndSendTransaction", () => {
168
+ it("should sign and send transaction", async () => {
169
+ const result = await service.signAndSendTransaction({ rawTransaction: "rawTx123" });
170
+ expect(result).toBe("signature123");
171
+ });
172
+
173
+ it("should return empty string when signedTx is empty", async () => {
174
+ mockAccountInfo.signTransaction = vi.fn().mockResolvedValue("");
175
+ const result = await service.signAndSendTransaction({ rawTransaction: "rawTx123" });
176
+ expect(result).toBe("");
177
+ });
178
+ });
179
+
180
+ describe("signAllTransactions", () => {
181
+ it("should throw error", async () => {
182
+ await expect(service.signAllTransactions({ rawTransactions: [] })).rejects.toThrow("no support");
183
+ });
184
+ });
185
+
186
+ describe("signAndSendAllTransactions", () => {
187
+ it("should throw error", async () => {
188
+ await expect(service.signAndSendAllTransactions({ rawTransactions: [] })).rejects.toThrow("no support");
189
+ });
190
+ });
191
+
192
+ describe("queryRent", () => {
193
+ it("should query rent for SOL transfer when account exists", async () => {
194
+ const { Connection } = await import("@solana/web3.js");
195
+ const mockConnection = new Connection("https://rpc.example.com");
196
+ vi.spyOn(mockConnection, "getAccountInfo").mockResolvedValue({ lamports: 1000000 } as any);
197
+
198
+ (service as any).connection = mockConnection;
199
+
200
+ const result = await service.queryRent({
201
+ toAddress: "SolanaAddress123",
202
+ });
203
+
204
+ expect(result.minTransferAmount).toBe("0");
205
+ expect(result.createSplTokenFee).toBeNull();
206
+ });
207
+
208
+ it("should query rent for token transfer when token account exists", async () => {
209
+ const { Connection } = await import("@solana/web3.js");
210
+ const mockConnection = new Connection("https://rpc.example.com");
211
+ vi.spyOn(mockConnection, "getAccountInfo").mockResolvedValue({ lamports: 1000000 } as any);
212
+
213
+ (service as any).connection = mockConnection;
214
+
215
+ const result = await service.queryRent({
216
+ toAddress: "SolanaAddress123",
217
+ tokenAddress: "TokenAddress123",
218
+ });
219
+
220
+ expect(result.minTransferAmount).toBe("0");
221
+ expect(result.createSplTokenFee).toBeNull();
222
+ });
223
+
224
+ it("should return default values on error", async () => {
225
+ const { Connection } = await import("@solana/web3.js");
226
+ const mockConnection = new Connection("https://rpc.example.com");
227
+ vi.spyOn(mockConnection, "getAccountInfo").mockRejectedValue(new Error("Network error"));
228
+
229
+ (service as any).connection = mockConnection;
230
+
231
+ const result = await service.queryRent({
232
+ toAddress: "SolanaAddress123",
233
+ });
234
+
235
+ expect(result.minTransferAmount).toBe("0.0009");
236
+ expect(result.createSplTokenFee).toBeNull();
237
+ });
238
+
239
+ it("should set createSplTokenFee when token account does not exist", async () => {
240
+ const { Connection } = await import("@solana/web3.js");
241
+ const mockConnection = new Connection("https://rpc.example.com");
242
+ vi.spyOn(mockConnection, "getAccountInfo").mockResolvedValue(null);
243
+
244
+ (service as any).connection = mockConnection;
245
+
246
+ const result = await service.queryRent({
247
+ toAddress: "SolanaAddress123",
248
+ tokenAddress: "TokenMint123",
249
+ });
250
+
251
+ expect(result.createSplTokenFee).toBe("0.001");
252
+ });
253
+
254
+ it("should set createSplTokenFee when getAssociatedTokenAddress throws", async () => {
255
+ const { getAssociatedTokenAddress } = await import("@solana/spl-token");
256
+ vi.mocked(getAssociatedTokenAddress).mockRejectedValue(new Error("Network error"));
257
+
258
+ const result = await service.queryRent({
259
+ toAddress: "SolanaAddress123",
260
+ tokenAddress: "TokenMint123",
261
+ });
262
+
263
+ expect(result.minTransferAmount).toBe("0");
264
+ expect(result.createSplTokenFee).toBe("0.001");
265
+ });
266
+
267
+ it("should return default rent info in catch when queryRent throws (covers lines 279-287)", async () => {
268
+ const result = await service.queryRent(null as any);
269
+ expect(result).toEqual({ minTransferAmount: "0", createSplTokenFee: null });
270
+ });
271
+ });
272
+
273
+ describe("_queryGasInfo", () => {
274
+ it("should return gas fees when queryGasInfo succeeds", async () => {
275
+ (service as any).transactions = {
276
+ queryGasInfo: vi.fn().mockResolvedValue({
277
+ success: true,
278
+ data: {
279
+ baseFee: 5000,
280
+ gasLimit: 200000,
281
+ priorityFeeLow: 100,
282
+ priorityFeeMedium: 200,
283
+ priorityFeeHigh: 300,
284
+ },
285
+ }),
286
+ };
287
+ (service as any).networks = {
288
+ getNetworkByChainId: vi.fn().mockResolvedValue({ nativeCurrency: { decimals: 9 } }),
289
+ };
290
+
291
+ const result = await service._queryGasInfo({
292
+ from: "From123",
293
+ to: "To123",
294
+ amount: 1,
295
+ chainId: "1",
296
+ });
297
+
298
+ expect(result.success).toBe(true);
299
+ expect(result.fees).toBeDefined();
300
+ expect(result.gasFee).toBeDefined();
301
+ });
302
+
303
+ it("should use tokenAddress and createTokenLegacyTransaction2 when tokenAddress provided", async () => {
304
+ const { createTokenLegacyTransaction2 } = await import("../utils");
305
+ (service as any).transactions = {
306
+ queryGasInfo: vi.fn().mockResolvedValue({
307
+ success: true,
308
+ data: {
309
+ baseFee: 5000,
310
+ gasLimit: 200000,
311
+ priorityFeeLow: 100,
312
+ priorityFeeMedium: 200,
313
+ priorityFeeHigh: 300,
314
+ },
315
+ }),
316
+ };
317
+ (service as any).networks = {
318
+ getNetworkByChainId: vi.fn().mockResolvedValue({ nativeCurrency: { decimals: 9 } }),
319
+ };
320
+
321
+ await service._queryGasInfo({
322
+ from: "From123",
323
+ to: "To123",
324
+ amount: 1,
325
+ chainId: "1",
326
+ tokenAddress: "TokenMint123",
327
+ });
328
+
329
+ expect(createTokenLegacyTransaction2).toHaveBeenCalled();
330
+ });
331
+
332
+ it("should return BaseConfig gasFee when queryGasInfo fails", async () => {
333
+ (service as any).transactions = {
334
+ queryGasInfo: vi.fn().mockResolvedValue({ success: false, message: "Error" }),
335
+ };
336
+
337
+ const result = await service._queryGasInfo({
338
+ from: "From123",
339
+ to: "To123",
340
+ amount: 1,
341
+ chainId: "1",
342
+ });
343
+
344
+ expect(result.success).toBe(true);
345
+ expect(result.gasFee).toBeDefined();
346
+ });
347
+ });
348
+
349
+ describe("sendSignedTx", () => {
350
+ it("should send signed transaction successfully", async () => {
351
+ const result = await service.sendSignedTx("hexTx123");
352
+ expect(result).toBe("signature123");
353
+ });
354
+
355
+ it("should use base58 when hex deserialize fails", async () => {
356
+ const { VersionedTransaction } = await import("@solana/web3.js");
357
+ const base = await import("../../dogecoin/base");
358
+ vi.mocked(VersionedTransaction.deserialize)
359
+ .mockImplementationOnce(() => {
360
+ throw new Error("invalid hex");
361
+ })
362
+ .mockImplementationOnce(() => ({
363
+ signatures: [{ toString: () => "sig123" }],
364
+ })) as any;
365
+ vi.spyOn(base, "toBase58").mockReturnValue("base58Encoded" as any);
366
+
367
+ const { Connection } = await import("@solana/web3.js");
368
+ const mockConn = new Connection("https://rpc.example.com");
369
+ vi.spyOn(mockConn, "simulateTransaction").mockResolvedValue({ value: { err: null } } as any);
370
+ (mockConn as any).getSignatureStatus = vi.fn().mockResolvedValue({ value: { confirmationStatus: "confirmed" } });
371
+ const sendAndConfirm = (await import("@solana/web3.js")).sendAndConfirmRawTransaction;
372
+ vi.mocked(sendAndConfirm).mockResolvedValue("signature123");
373
+ (service as any).connection = mockConn;
374
+
375
+ const result = await service.sendSignedTx("notValidHex");
376
+ expect(result).toBe("signature123");
377
+ });
378
+
379
+ it("should throw error when transaction lacks signature", async () => {
380
+ const { VersionedTransaction } = await import("@solana/web3.js");
381
+ vi.mocked(VersionedTransaction.deserialize).mockReturnValue({
382
+ signatures: [],
383
+ } as any);
384
+
385
+ await expect(service.sendSignedTx("hexTx123")).rejects.toThrow("lack of sign");
386
+ });
387
+
388
+ it("should handle transaction simulation error", async () => {
389
+ const { Connection, VersionedTransaction } = await import("@solana/web3.js");
390
+ const mockConnection = new Connection("https://rpc.example.com");
391
+
392
+ vi.mocked(VersionedTransaction.deserialize).mockReturnValue({
393
+ signatures: [{ toString: () => "signature123" }],
394
+ } as any);
395
+
396
+ vi.spyOn(mockConnection, "simulateTransaction").mockResolvedValue({
397
+ value: { err: "InsufficientFunds", logs: ["Error log"] },
398
+ } as any);
399
+
400
+ (service as any).connection = mockConnection;
401
+
402
+ await expect(service.sendSignedTx("hexTx123")).rejects.toThrow("Transaction failed");
403
+ });
404
+
405
+ it("should return signature when TransactionExpiredTimeoutError but status is confirmed", async () => {
406
+ const { Connection, VersionedTransaction } = await import("@solana/web3.js");
407
+ const mockConnection = new Connection("https://rpc.example.com");
408
+ (mockConnection as any).getSignatureStatus = vi.fn().mockResolvedValue({
409
+ value: { confirmationStatus: "confirmed" },
410
+ });
411
+
412
+ vi.mocked(VersionedTransaction.deserialize).mockReturnValue({
413
+ signatures: [{ toString: () => "sig456" }],
414
+ } as any);
415
+ vi.spyOn(mockConnection, "simulateTransaction").mockResolvedValue({ value: { err: null } } as any);
416
+ const sendAndConfirm = (await import("@solana/web3.js")).sendAndConfirmRawTransaction;
417
+ vi.mocked(sendAndConfirm).mockRejectedValue(new Error("TransactionExpiredTimeoutError"));
418
+
419
+ (service as any).connection = mockConnection;
420
+
421
+ const result = await service.sendSignedTx("hexTx123");
422
+ expect(result).toBe("sig456");
423
+ });
424
+ });
425
+ });