@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
@@ -0,0 +1,937 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ isAddress,
4
+ txToHex,
5
+ hexToTx,
6
+ hexToUint8Array,
7
+ createTransaction,
8
+ createLegacyTx,
9
+ createTokenLegacyTransaction,
10
+ createTokenLegacyTransaction2,
11
+ createOkxTxData,
12
+ estimateFees,
13
+ getBalances,
14
+ getTokenBalances,
15
+ getTokenDetailFromRpc,
16
+ decodeMetadata,
17
+ buildSubmitTxParams,
18
+ parseSolTx,
19
+ parseSolLegacyTx,
20
+ } from "../utils";
21
+ import { Connection, Transaction, VersionedTransaction } from "@solana/web3.js";
22
+ import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
23
+
24
+ // Mock Solana modules
25
+ vi.mock("@solana/web3.js", async () => {
26
+ const actual = await vi.importActual("@solana/web3.js");
27
+
28
+ class MockConnection {
29
+ getLatestBlockhash = vi.fn().mockResolvedValue({
30
+ blockhash: "blockhash123",
31
+ lastValidBlockHeight: 100,
32
+ });
33
+ getMultipleAccountsInfo = vi.fn().mockResolvedValue([{ lamports: 1000000 }]);
34
+ getParsedTokenAccountsByOwner = vi.fn().mockResolvedValue({
35
+ value: [
36
+ {
37
+ account: {
38
+ data: {
39
+ parsed: {
40
+ info: {
41
+ mint: "TokenMint123",
42
+ tokenAmount: {
43
+ amount: "1000000",
44
+ decimals: 6,
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ ],
52
+ });
53
+ getAccountInfo = vi.fn().mockResolvedValue({
54
+ data: Buffer.from("metadata"),
55
+ });
56
+ getFeeForMessage = vi.fn().mockResolvedValue({ value: 5000 });
57
+ constructor(
58
+ public rpcUrl: string,
59
+ public config?: any,
60
+ ) {}
61
+ }
62
+
63
+ return {
64
+ ...actual,
65
+ Connection: MockConnection,
66
+ PublicKey: class {
67
+ constructor(public address: string) {
68
+ if (address === "invalid") {
69
+ throw new Error("Invalid public key");
70
+ }
71
+ }
72
+ toString() {
73
+ return this.address;
74
+ }
75
+ toBuffer() {
76
+ return Buffer.alloc(32, 0);
77
+ }
78
+ static findProgramAddressSync = vi.fn().mockReturnValue([{ toString: () => "pda123" }]);
79
+ },
80
+ Transaction: class {
81
+ add = vi.fn().mockReturnThis();
82
+ feePayer: any = null;
83
+ recentBlockhash: any = null;
84
+ lastValidBlockHeight: any = null;
85
+ serialize = vi.fn().mockReturnValue(Buffer.from("serialized"));
86
+ compileMessage = vi.fn().mockReturnValue({});
87
+ instructions: any[] = [];
88
+ static from = vi.fn().mockImplementation((buffer: Buffer) => {
89
+ // Only throw error for truly invalid buffers (empty or null)
90
+ // Valid test cases should succeed
91
+ if (!buffer || buffer.length === 0) {
92
+ throw new Error("Invalid transaction");
93
+ }
94
+ return new (Transaction as any)();
95
+ });
96
+ },
97
+ VersionedTransaction: class {
98
+ constructor(public message: any) {}
99
+ serialize = vi.fn().mockReturnValue(Buffer.from("serialized"));
100
+ static deserialize = vi.fn().mockImplementation((buffer: Buffer) => {
101
+ // Only throw error for truly invalid buffers (empty or null)
102
+ // Valid test cases should succeed
103
+ if (!buffer || buffer.length === 0) {
104
+ throw new Error("Invalid transaction");
105
+ }
106
+ return {
107
+ message: {
108
+ instructions: [],
109
+ },
110
+ signatures: [],
111
+ };
112
+ });
113
+ },
114
+ TransactionMessage: class {
115
+ constructor(public params: any) {}
116
+ compileToV0Message = vi.fn().mockReturnValue({
117
+ instructions: [],
118
+ });
119
+ static decompile = vi.fn().mockReturnValue({
120
+ instructions: [],
121
+ });
122
+ },
123
+ SystemProgram: {
124
+ transfer: vi.fn().mockReturnValue({}),
125
+ programId: { toString: () => "SystemProgram" },
126
+ },
127
+ ComputeBudgetProgram: {
128
+ setComputeUnitPrice: vi.fn().mockReturnValue({}),
129
+ },
130
+ LAMPORTS_PER_SOL: 1000000000,
131
+ };
132
+ });
133
+
134
+ // Mock @solana/spl-token
135
+ vi.mock("@solana/spl-token", () => {
136
+ class TokenAccountNotFoundError extends Error {
137
+ name = "TokenAccountNotFoundError";
138
+ }
139
+ class TokenInvalidAccountOwnerError extends Error {
140
+ name = "TokenInvalidAccountOwnerError";
141
+ }
142
+ return {
143
+ getAssociatedTokenAddress: vi.fn().mockResolvedValue("tokenAddress123"),
144
+ getAccount: vi.fn().mockResolvedValue({
145
+ mint: { toString: () => "TokenMint123" },
146
+ owner: { toString: () => "Owner123" },
147
+ }),
148
+ createTransferInstruction: vi.fn().mockReturnValue({}),
149
+ createAssociatedTokenAccountInstruction: vi.fn().mockReturnValue({}),
150
+ TOKEN_PROGRAM_ID: { toString: () => "TokenProgram" },
151
+ ASSOCIATED_TOKEN_PROGRAM_ID: { toString: () => "AssociatedTokenProgram" },
152
+ TokenAccountNotFoundError,
153
+ TokenInvalidAccountOwnerError,
154
+ };
155
+ });
156
+
157
+ /** Build a buffer that decodeMetadata can parse (name, symbol, uri, optional creators, decimals). */
158
+ function buildMetadataBuffer(opts: { name: string; symbol: string; uri: string; creatorLength?: number; decimals?: number }) {
159
+ const { name, symbol, uri, creatorLength = 0, decimals = 6 } = opts;
160
+ const base = 1 + 32 + 32;
161
+ const nameBuf = Buffer.from(name, "utf8");
162
+ const symbolBuf = Buffer.from(symbol, "utf8");
163
+ const uriBuf = Buffer.from(uri, "utf8");
164
+ const size = base + 4 + nameBuf.length + 4 + symbolBuf.length + 4 + uriBuf.length + 2 + 1 + creatorLength * 34 + 1;
165
+ const buffer = Buffer.alloc(Math.max(size, 200));
166
+ let offset = base;
167
+ buffer.writeUInt32LE(nameBuf.length, offset);
168
+ offset += 4;
169
+ nameBuf.copy(buffer, offset);
170
+ offset += nameBuf.length;
171
+ buffer.writeUInt32LE(symbolBuf.length, offset);
172
+ offset += 4;
173
+ symbolBuf.copy(buffer, offset);
174
+ offset += symbolBuf.length;
175
+ buffer.writeUInt32LE(uriBuf.length, offset);
176
+ offset += 4;
177
+ uriBuf.copy(buffer, offset);
178
+ offset += uriBuf.length;
179
+ offset += 2; // seller fee
180
+ buffer[offset] = creatorLength;
181
+ offset += 1;
182
+ offset += creatorLength * 34;
183
+ buffer[offset] = decimals;
184
+ return buffer;
185
+ }
186
+
187
+ describe("solana/utils", () => {
188
+ let mockConnection: Connection;
189
+
190
+ beforeEach(() => {
191
+ vi.clearAllMocks();
192
+ mockConnection = new Connection("https://rpc.example.com");
193
+ });
194
+
195
+ describe("isAddress", () => {
196
+ it("should return true for valid address", () => {
197
+ expect(isAddress("11111111111111111111111111111111")).toBe(true);
198
+ });
199
+
200
+ it("should return false for invalid address", () => {
201
+ expect(isAddress("invalid")).toBe(false);
202
+ });
203
+ });
204
+
205
+ describe("txToHex", () => {
206
+ it("should convert VersionedTransaction to hex", () => {
207
+ const tx = new VersionedTransaction({} as any);
208
+ const hex = txToHex(tx);
209
+ expect(hex).toBeDefined();
210
+ });
211
+
212
+ it("should convert Transaction to hex", () => {
213
+ const tx = new Transaction();
214
+ const hex = txToHex(tx);
215
+ expect(hex).toBeDefined();
216
+ });
217
+
218
+ it("should return tx as-is when not VersionedTransaction or Transaction", () => {
219
+ const other = { custom: true };
220
+ const result = txToHex(other as any);
221
+ expect(result).toBe(other);
222
+ });
223
+ });
224
+
225
+ describe("hexToTx", () => {
226
+ it("should convert hex to VersionedTransaction", () => {
227
+ const hex = "01020304";
228
+ const tx = hexToTx(hex);
229
+ expect(tx).toBeDefined();
230
+ });
231
+
232
+ it("should throw error for invalid hex", async () => {
233
+ // Mock VersionedTransaction.deserialize and Transaction.from to throw errors for invalid hex
234
+ const { VersionedTransaction, Transaction } = await import("@solana/web3.js");
235
+ vi.mocked(VersionedTransaction.deserialize).mockImplementation((buffer: Buffer) => {
236
+ // Check if buffer is invalid - "invalid" as hex creates a buffer, but deserialize will fail
237
+ // We'll simulate this by checking if the hex string would create an invalid transaction
238
+ // For this test, we'll make it throw for the specific "invalid" case
239
+ if (!buffer || buffer.length === 0) {
240
+ throw new Error("Invalid transaction");
241
+ }
242
+ // For "invalid" hex string, the buffer will exist but deserialization will fail
243
+ // We simulate this by checking the buffer content or length
244
+ // Since "invalid" creates a valid buffer, we need a different approach
245
+ // Let's make it throw for very short buffers that would be invalid transactions
246
+ if (buffer.length < 2) {
247
+ throw new Error("Invalid transaction");
248
+ }
249
+ return {
250
+ message: { instructions: [] },
251
+ signatures: [],
252
+ };
253
+ });
254
+ vi.mocked(Transaction.from).mockImplementation((buffer: Buffer) => {
255
+ // Same logic for Transaction.from
256
+ if (!buffer || buffer.length === 0) {
257
+ throw new Error("Invalid transaction");
258
+ }
259
+ if (buffer.length < 2) {
260
+ throw new Error("Invalid transaction");
261
+ }
262
+ return new Transaction();
263
+ });
264
+
265
+ // For "invalid" hex, Buffer.from("invalid", "hex") creates a buffer
266
+ // But we want it to fail, so we'll make the mock throw for this specific case
267
+ // Actually, "invalid" as hex creates a valid buffer of length 3
268
+ // So we need to make the mock throw for this case
269
+ vi.mocked(VersionedTransaction.deserialize).mockImplementationOnce((buffer: Buffer) => {
270
+ throw new Error("Invalid transaction");
271
+ });
272
+ vi.mocked(Transaction.from).mockImplementationOnce((buffer: Buffer) => {
273
+ throw new Error("Invalid transaction");
274
+ });
275
+
276
+ expect(() => hexToTx("invalid")).toThrow("Failed to deserialize transaction");
277
+ });
278
+ });
279
+
280
+ describe("hexToUint8Array", () => {
281
+ it("should convert hex to Uint8Array", () => {
282
+ const hex = "0x" + "00".repeat(64);
283
+ const array = hexToUint8Array(hex);
284
+ expect(array.length).toBe(64);
285
+ });
286
+
287
+ it("should handle hex without 0x prefix", () => {
288
+ const hex = "00".repeat(64);
289
+ const array = hexToUint8Array(hex);
290
+ expect(array.length).toBe(64);
291
+ });
292
+
293
+ it("should throw error for invalid length", () => {
294
+ expect(() => hexToUint8Array("0x1234")).toThrow("Invalid signature length");
295
+ });
296
+ });
297
+
298
+ describe("createTransaction", () => {
299
+ it("should create transaction", async () => {
300
+ const tx = await createTransaction(
301
+ {
302
+ from: "11111111111111111111111111111111",
303
+ to: "22222222222222222222222222222222",
304
+ amount: 1.0,
305
+ priorityFee: 1000,
306
+ },
307
+ mockConnection,
308
+ );
309
+ expect(tx).toBeDefined();
310
+ });
311
+
312
+ it("should create transaction without priority fee", async () => {
313
+ const tx = await createTransaction(
314
+ {
315
+ from: "11111111111111111111111111111111",
316
+ to: "22222222222222222222222222222222",
317
+ amount: 1.0,
318
+ },
319
+ mockConnection,
320
+ );
321
+ expect(tx).toBeDefined();
322
+ });
323
+ });
324
+
325
+ describe("createLegacyTx", () => {
326
+ it("should create legacy transaction", async () => {
327
+ const tx = await createLegacyTx(
328
+ {
329
+ from: "11111111111111111111111111111111",
330
+ to: "22222222222222222222222222222222",
331
+ amount: 1.0,
332
+ },
333
+ mockConnection,
334
+ );
335
+ expect(tx).toBeDefined();
336
+ });
337
+ });
338
+
339
+ describe("createTokenLegacyTransaction", () => {
340
+ it("should create token transaction when account exists", async () => {
341
+ const { getAccount } = await import("@solana/spl-token");
342
+ vi.mocked(getAccount).mockResolvedValue({
343
+ mint: { toString: () => "TokenMint123" },
344
+ owner: { toString: () => "Owner123" },
345
+ } as any);
346
+
347
+ const tx = await createTokenLegacyTransaction(
348
+ {
349
+ from: "11111111111111111111111111111111",
350
+ to: "22222222222222222222222222222222",
351
+ amount: 1.0,
352
+ tokenAddress: "TokenMint123",
353
+ decimals: 6,
354
+ },
355
+ mockConnection,
356
+ );
357
+ expect(tx).toBeDefined();
358
+ });
359
+
360
+ it("should add createAssociatedTokenAccountInstruction when TokenAccountNotFoundError", async () => {
361
+ const { getAccount, createAssociatedTokenAccountInstruction, TokenAccountNotFoundError } =
362
+ await import("@solana/spl-token");
363
+ vi.mocked(getAccount).mockRejectedValue(new TokenAccountNotFoundError());
364
+
365
+ const tx = await createTokenLegacyTransaction(
366
+ {
367
+ from: "11111111111111111111111111111111",
368
+ to: "22222222222222222222222222222222",
369
+ amount: 1.0,
370
+ tokenAddress: "TokenMint123",
371
+ decimals: 6,
372
+ },
373
+ mockConnection,
374
+ );
375
+ expect(tx).toBeDefined();
376
+ expect(createAssociatedTokenAccountInstruction).toHaveBeenCalled();
377
+ });
378
+
379
+ it("should throw when getAccount throws non-TokenAccountNotFoundError (covers lines 179, 193-194)", async () => {
380
+ const { getAccount } = await import("@solana/spl-token");
381
+ vi.mocked(getAccount).mockRejectedValueOnce(new Error("RPC error"));
382
+
383
+ await expect(
384
+ createTokenLegacyTransaction(
385
+ {
386
+ from: "11111111111111111111111111111111",
387
+ to: "22222222222222222222222222222222",
388
+ amount: 1.0,
389
+ tokenAddress: "TokenMint123",
390
+ decimals: 6,
391
+ },
392
+ mockConnection,
393
+ ),
394
+ ).rejects.toThrow("RPC error");
395
+ });
396
+ });
397
+
398
+ describe("createTokenLegacyTransaction2", () => {
399
+ it("should create token legacy transaction", async () => {
400
+ const { createTokenLegacyTransaction2 } = await import("../utils");
401
+ const tx = await createTokenLegacyTransaction2(
402
+ {
403
+ from: "11111111111111111111111111111111",
404
+ to: "22222222222222222222222222222222",
405
+ amount: 1.0,
406
+ tokenAddress: "TokenMint123",
407
+ decimals: 6,
408
+ },
409
+ mockConnection,
410
+ );
411
+ expect(tx).toBeDefined();
412
+ });
413
+ });
414
+
415
+ describe("createOkxTxData", () => {
416
+ it("should return sendSolana params", async () => {
417
+ const { createOkxTxData } = await import("../utils");
418
+ const result = await createOkxTxData(
419
+ {
420
+ from: "11111111111111111111111111111111",
421
+ to: "22222222222222222222222222222222",
422
+ amount: 1.0,
423
+ },
424
+ "sendSolana",
425
+ mockConnection,
426
+ );
427
+ expect(result.type).toBe("transfer");
428
+ expect(result.blockHash).toBeDefined();
429
+ });
430
+ });
431
+
432
+ describe("estimateFees", () => {
433
+ it("should estimate fees for SOL transfer", async () => {
434
+ const fees = await estimateFees(
435
+ {
436
+ txData: {
437
+ from: "11111111111111111111111111111111",
438
+ to: "22222222222222222222222222222222",
439
+ amount: 1.0,
440
+ },
441
+ priorityFee: 1000,
442
+ computeUnits: 200000,
443
+ },
444
+ mockConnection,
445
+ );
446
+ expect(fees.baseFee).toBeDefined();
447
+ expect(fees.priorityFee).toBeDefined();
448
+ expect(fees.totalFee).toBeDefined();
449
+ });
450
+
451
+ it("should estimate fees for token transfer", async () => {
452
+ const fees = await estimateFees(
453
+ {
454
+ txData: {
455
+ from: "11111111111111111111111111111111",
456
+ to: "22222222222222222222222222222222",
457
+ amount: 1.0,
458
+ tokenAddress: "TokenMint123",
459
+ },
460
+ priorityFee: 1000,
461
+ computeUnits: 200000,
462
+ },
463
+ mockConnection,
464
+ );
465
+ expect(fees.baseFee).toBeDefined();
466
+ });
467
+ });
468
+
469
+ describe("getBalances", () => {
470
+ it("should get balances", async () => {
471
+ const balances = await getBalances(["11111111111111111111111111111111"], mockConnection);
472
+ expect(balances).toBeDefined();
473
+ expect(balances["11111111111111111111111111111111"]).toBeDefined();
474
+ });
475
+ });
476
+
477
+ describe("getTokenBalances", () => {
478
+ it("should get token balances", async () => {
479
+ const balances = await getTokenBalances("11111111111111111111111111111111", mockConnection);
480
+ expect(balances).toBeDefined();
481
+ });
482
+ });
483
+
484
+ describe("getTokenDetailFromRpc", () => {
485
+ it("should get token detail from RPC", async () => {
486
+ global.fetch = vi.fn().mockResolvedValue({
487
+ json: vi.fn().mockResolvedValue({ image: "icon.png" }),
488
+ });
489
+
490
+ const tokenInfo = await getTokenDetailFromRpc("TokenMint123", mockConnection);
491
+ expect(tokenInfo).toBeDefined();
492
+ });
493
+
494
+ it("should return token info with name, symbol, decimals and icon from metadata URI", async () => {
495
+ const metaBuffer = buildMetadataBuffer({
496
+ name: "TestToken",
497
+ symbol: "TST",
498
+ uri: "https://example.com/meta.json",
499
+ decimals: 9,
500
+ });
501
+ vi.mocked(mockConnection.getAccountInfo).mockResolvedValue({ data: metaBuffer } as any);
502
+ global.fetch = vi.fn().mockResolvedValue({
503
+ json: vi.fn().mockResolvedValue({ image: "https://example.com/icon.png" }),
504
+ });
505
+
506
+ const tokenInfo = await getTokenDetailFromRpc("MetaTokenMint", mockConnection);
507
+
508
+ expect(tokenInfo).toBeDefined();
509
+ expect(tokenInfo.name).toBe("TestToken");
510
+ expect(tokenInfo.symbol).toBe("TST");
511
+ expect(tokenInfo.decimals).toBe(9);
512
+ expect(tokenInfo.icon).toBe("https://example.com/icon.png");
513
+ });
514
+
515
+ it("should return cached token info on second call (same address)", async () => {
516
+ const metaBuffer = buildMetadataBuffer({ name: "C", symbol: "C", uri: "" });
517
+ vi.mocked(mockConnection.getAccountInfo).mockResolvedValue({ data: metaBuffer } as any);
518
+
519
+ await getTokenDetailFromRpc("CachedMint", mockConnection);
520
+ const tokenInfo = await getTokenDetailFromRpc("CachedMint", mockConnection);
521
+
522
+ expect(tokenInfo).toBeDefined();
523
+ expect(tokenInfo.name).toBe("C");
524
+ expect(mockConnection.getAccountInfo).toHaveBeenCalledTimes(1);
525
+ });
526
+
527
+ it("should set icon to empty when fetch of metadata URI fails", async () => {
528
+ const metaBuffer = buildMetadataBuffer({
529
+ name: "F",
530
+ symbol: "F",
531
+ uri: "https://example.com/fail",
532
+ });
533
+ vi.mocked(mockConnection.getAccountInfo).mockResolvedValue({ data: metaBuffer } as any);
534
+ global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
535
+
536
+ const tokenInfo = await getTokenDetailFromRpc("FetchFailMint", mockConnection);
537
+
538
+ expect(tokenInfo).toBeDefined();
539
+ expect(tokenInfo.icon).toBe("");
540
+ });
541
+
542
+ it("should return null on error", async () => {
543
+ const conn = new Connection("https://rpc.example.com");
544
+ vi.mocked(conn.getAccountInfo).mockResolvedValue(null);
545
+ const uniqueAddress = "NonexistentTokenMintForNullTest";
546
+
547
+ const tokenInfo = await getTokenDetailFromRpc(uniqueAddress, conn);
548
+ expect(tokenInfo).toBeNull();
549
+ });
550
+
551
+ it("should return null when getTokenDetailFromRpc throws (covers catch 388-389)", async () => {
552
+ const tokenInfo = await getTokenDetailFromRpc("invalid", mockConnection);
553
+ expect(tokenInfo).toBeNull();
554
+ });
555
+ });
556
+
557
+ describe("decodeMetadata", () => {
558
+ it("should decode metadata from buffer", () => {
559
+ // Create a mock buffer with metadata structure
560
+ const buffer = Buffer.alloc(200);
561
+ let offset = 1 + 32 + 32; // Skip key, updateAuthority, mint
562
+
563
+ // Write name length and string
564
+ buffer.writeUInt32LE(4, offset);
565
+ offset += 4;
566
+ buffer.write("Test", offset);
567
+ offset += 4;
568
+
569
+ // Write symbol length and string
570
+ buffer.writeUInt32LE(3, offset);
571
+ offset += 4;
572
+ buffer.write("TST", offset);
573
+ offset += 3;
574
+
575
+ // Write URI length and string
576
+ buffer.writeUInt32LE(10, offset);
577
+ offset += 4;
578
+ buffer.write("https://...", offset);
579
+ offset += 10;
580
+
581
+ // Write seller fee (2 bytes)
582
+ offset += 2;
583
+
584
+ // Write creator length (1 byte)
585
+ buffer[offset] = 0;
586
+ offset += 1;
587
+
588
+ // Write decimals (1 byte)
589
+ buffer[offset] = 9;
590
+
591
+ const metadata = decodeMetadata(buffer);
592
+ expect(metadata.name).toBe("Test");
593
+ expect(metadata.symbol).toBe("TST");
594
+ expect(metadata.decimals).toBe(9);
595
+ });
596
+
597
+ it("should return default values on error", () => {
598
+ const buffer = Buffer.alloc(10);
599
+ const metadata = decodeMetadata(buffer);
600
+ expect(metadata.name).toBe("");
601
+ expect(metadata.symbol).toBe("");
602
+ expect(metadata.uri).toBe("");
603
+ expect(metadata.decimals).toBe(0);
604
+ });
605
+
606
+ it("should return default when string length > 1000", () => {
607
+ const buffer = Buffer.alloc(2000);
608
+ let offset = 1 + 32 + 32;
609
+ buffer.writeUInt32LE(1500, offset);
610
+ const metadata = decodeMetadata(buffer);
611
+ expect(metadata.name).toBe("");
612
+ expect(metadata.decimals).toBe(0);
613
+ });
614
+
615
+ it("should skip creators when creatorLength > 0", () => {
616
+ const buffer = buildMetadataBuffer({
617
+ name: "N",
618
+ symbol: "S",
619
+ uri: "",
620
+ creatorLength: 1,
621
+ decimals: 8,
622
+ });
623
+ const metadata = decodeMetadata(buffer);
624
+ expect(metadata.name).toBe("N");
625
+ expect(metadata.symbol).toBe("S");
626
+ expect(metadata.decimals).toBe(8);
627
+ });
628
+ });
629
+
630
+ describe("buildSubmitTxParams", () => {
631
+ it("should build submit tx params", async () => {
632
+ const params = await buildSubmitTxParams(
633
+ {
634
+ from: "11111111111111111111111111111111",
635
+ to: "22222222222222222222222222222222",
636
+ amount: 1.0,
637
+ chainIndex: "100",
638
+ },
639
+ "0xcallData",
640
+ );
641
+ expect(params.fromAddress).toBe("11111111111111111111111111111111");
642
+ expect(params.callData).toBe("0xcallData");
643
+ });
644
+
645
+ it("should use submitType when provided", async () => {
646
+ const { SubmitParamsType } = await import("../../types");
647
+ const params = await buildSubmitTxParams(
648
+ {
649
+ from: "11111111111111111111111111111111",
650
+ to: "22222222222222222222222222222222",
651
+ amount: 1.0,
652
+ chainIndex: "100",
653
+ submitType: SubmitParamsType.SWAP,
654
+ },
655
+ "0xcallData",
656
+ );
657
+ expect(params.type).toBe(SubmitParamsType.SWAP);
658
+ });
659
+ });
660
+
661
+ describe("createTokenLegacyTransaction inner catch", () => {
662
+ it("should continue when createAssociatedTokenAccountInstruction throws after TokenAccountNotFoundError", async () => {
663
+ const { getAccount, createAssociatedTokenAccountInstruction, TokenAccountNotFoundError } =
664
+ await import("@solana/spl-token");
665
+ vi.mocked(getAccount).mockRejectedValue(new TokenAccountNotFoundError());
666
+ vi.mocked(createAssociatedTokenAccountInstruction).mockImplementation(() => {
667
+ throw new Error("create ATA failed");
668
+ });
669
+
670
+ const tx = await createTokenLegacyTransaction(
671
+ {
672
+ from: "11111111111111111111111111111111",
673
+ to: "22222222222222222222222222222222",
674
+ amount: 1.0,
675
+ tokenAddress: "TokenMint123",
676
+ decimals: 6,
677
+ },
678
+ mockConnection,
679
+ );
680
+ expect(tx).toBeDefined();
681
+ });
682
+ });
683
+
684
+ describe("createTokenLegacyTransaction2", () => {
685
+ it("should throw when createAssociatedTokenAccountInstruction throws", async () => {
686
+ const { createAssociatedTokenAccountInstruction } = await import("@solana/spl-token");
687
+ vi.mocked(createAssociatedTokenAccountInstruction).mockImplementation(() => {
688
+ throw new Error("ATA failed");
689
+ });
690
+
691
+ await expect(
692
+ createTokenLegacyTransaction2(
693
+ {
694
+ from: "11111111111111111111111111111111",
695
+ to: "22222222222222222222222222222222",
696
+ amount: 1.0,
697
+ tokenAddress: "TokenMint123",
698
+ decimals: 6,
699
+ },
700
+ mockConnection,
701
+ ),
702
+ ).rejects.toThrow("ATA failed");
703
+ });
704
+ });
705
+
706
+ describe("createOkxTxData sendToken", () => {
707
+ it("should return tokenTransfer params when method is sendToken", async () => {
708
+ const metaBuffer = buildMetadataBuffer({ name: "T", symbol: "T", uri: "", decimals: 6 });
709
+ vi.mocked(mockConnection.getAccountInfo).mockResolvedValue({ data: metaBuffer } as any);
710
+
711
+ const result = await createOkxTxData(
712
+ {
713
+ from: "11111111111111111111111111111111",
714
+ to: "22222222222222222222222222222222",
715
+ amount: 100,
716
+ tokenAddress: "Mint456",
717
+ },
718
+ "sendToken",
719
+ mockConnection,
720
+ );
721
+
722
+ expect(result.type).toBe("tokenTransfer");
723
+ expect(result.mint).toBe("Mint456");
724
+ expect(result.from).toBe("11111111111111111111111111111111");
725
+ expect(result.to).toBe("22222222222222222222222222222222");
726
+ });
727
+ });
728
+
729
+ describe("parseSolTx and parseSolLegacyTx", () => {
730
+ it("should parse legacy transaction with SPL token transfer instruction", async () => {
731
+ const { getAccount } = await import("@solana/spl-token");
732
+ const fromAcc = { mint: { toString: () => "Mint1" }, owner: { toString: () => "OwnerFrom" } };
733
+ const toAcc = { mint: { toString: () => "Mint1" }, owner: { toString: () => "OwnerTo" } };
734
+ vi.mocked(getAccount)
735
+ .mockResolvedValueOnce(toAcc as any)
736
+ .mockResolvedValueOnce(fromAcc as any);
737
+
738
+ const fromKey = { pubkey: { toString: () => "OwnerFrom" } };
739
+ const toKey = { pubkey: { toString: () => "OwnerTo" } };
740
+ const data = Buffer.alloc(9);
741
+ data[0] = 3; // Transfer
742
+ data.writeBigUInt64LE(BigInt(1000), 1);
743
+
744
+ const tx = {
745
+ instructions: [
746
+ {
747
+ programId: { equals: (id: any) => id?.toString?.() === TOKEN_PROGRAM_ID?.toString?.() },
748
+ keys: [fromKey, toKey],
749
+ data,
750
+ },
751
+ ],
752
+ };
753
+
754
+ const result = await parseSolLegacyTx(tx as any, mockConnection);
755
+ expect(result.from).toBe("OwnerFrom");
756
+ expect(result.to).toBe("OwnerTo");
757
+ expect(result.amount).toBe(1000);
758
+ expect(result.tokenAddress).toBe("Mint1");
759
+ });
760
+
761
+ it("should parse legacy transaction with ATA instruction and system transfer", async () => {
762
+ const { getAccount } = await import("@solana/spl-token");
763
+ const { SystemProgram } = await import("@solana/web3.js");
764
+ vi.mocked(getAccount).mockRejectedValue(new Error("no account"));
765
+
766
+ const mintKey = { pubkey: { toString: () => "MintAddr" } };
767
+ const ownerKey = { pubkey: { toString: () => "OwnerTo" } };
768
+ const ataInstruction = {
769
+ programId: {
770
+ equals: (id: any) => id?.toString?.() === ASSOCIATED_TOKEN_PROGRAM_ID?.toString?.(),
771
+ },
772
+ keys: [{}, {}, ownerKey, mintKey],
773
+ data: Buffer.alloc(0),
774
+ };
775
+
776
+ const sysData = Buffer.alloc(12);
777
+ sysData[0] = 2; // Transfer
778
+ sysData.writeBigUInt64LE(BigInt(1e9), 4);
779
+ const sysInstruction = {
780
+ programId: {
781
+ equals: (id: any) => id === SystemProgram.programId || id?.toString?.() === SystemProgram.programId?.toString?.(),
782
+ },
783
+ keys: [
784
+ { pubkey: { toString: () => "FromSys" } },
785
+ { pubkey: { toString: () => "ToSys" } },
786
+ ],
787
+ data: sysData,
788
+ };
789
+
790
+ const tx = {
791
+ instructions: [ataInstruction, sysInstruction],
792
+ };
793
+
794
+ const result = await parseSolLegacyTx(tx as any, mockConnection);
795
+ expect(result.tokenAddress).toBe("MintAddr");
796
+ expect(result.from).toBe("FromSys");
797
+ expect(result.to).toBe("ToSys");
798
+ expect(result.amount).toBe(1e9);
799
+ });
800
+
801
+ it("should parse legacy token tx when getAccount fails for toTokenAccount (covers line 510)", async () => {
802
+ const { getAccount } = await import("@solana/spl-token");
803
+ vi.mocked(getAccount)
804
+ .mockRejectedValueOnce(new Error("to account not found"))
805
+ .mockResolvedValueOnce({ owner: { toString: () => "OwnerFrom" }, mint: {} } as any);
806
+
807
+ const fromKey = { pubkey: { toString: () => "OwnerFrom" } };
808
+ const toKey = { pubkey: { toString: () => "OwnerTo" } };
809
+ const data = Buffer.alloc(9);
810
+ data[0] = 3;
811
+ data.writeBigUInt64LE(BigInt(500), 1);
812
+
813
+ const tx = {
814
+ instructions: [
815
+ {
816
+ programId: { equals: (id: any) => id?.toString?.() === TOKEN_PROGRAM_ID?.toString?.() },
817
+ keys: [fromKey, toKey],
818
+ data,
819
+ },
820
+ ],
821
+ };
822
+
823
+ const result = await parseSolLegacyTx(tx as any, mockConnection);
824
+ expect(result.amount).toBe(500);
825
+ expect(result.from).toBe("OwnerFrom");
826
+ });
827
+
828
+ it("should parse legacy token tx when getAccount fails for fromTokenAccount (covers line 519)", async () => {
829
+ const { getAccount } = await import("@solana/spl-token");
830
+ vi.mocked(getAccount)
831
+ .mockResolvedValueOnce({ mint: { toString: () => "Mint1" }, owner: { toString: () => "ToOwner" } } as any)
832
+ .mockRejectedValueOnce(new Error("from account not found"));
833
+
834
+ const fromKey = { pubkey: { toString: () => "FromKey" } };
835
+ const toKey = { pubkey: { toString: () => "ToKey" } };
836
+ const data = Buffer.alloc(9);
837
+ data[0] = 3;
838
+ data.writeBigUInt64LE(BigInt(100), 1);
839
+
840
+ const tx = {
841
+ instructions: [
842
+ {
843
+ programId: { equals: (id: any) => id?.toString?.() === TOKEN_PROGRAM_ID?.toString?.() },
844
+ keys: [fromKey, toKey],
845
+ data,
846
+ },
847
+ ],
848
+ };
849
+
850
+ const result = await parseSolLegacyTx(tx as any, mockConnection);
851
+ expect(result.amount).toBe(100);
852
+ expect(result.to).toBe("ToOwner");
853
+ });
854
+ });
855
+
856
+ describe("parseSolTx versioned path", () => {
857
+ it("should return parsed data from versioned tx when decompile yields token transfer", async () => {
858
+ const { TransactionMessage } = await import("@solana/web3.js");
859
+ vi.mocked(TransactionMessage.decompile).mockReturnValue({
860
+ instructions: [
861
+ {
862
+ programId: { toString: () => TOKEN_PROGRAM_ID.toString() },
863
+ keys: [
864
+ { pubkey: { toString: () => "FromV" } },
865
+ { pubkey: { toString: () => "ToV" } },
866
+ ],
867
+ data: (() => {
868
+ const b = Buffer.alloc(9);
869
+ b[0] = 3;
870
+ b.writeBigUInt64LE(BigInt(2000), 1);
871
+ return b;
872
+ })(),
873
+ },
874
+ ],
875
+ } as any);
876
+
877
+ const vTx = new VersionedTransaction({} as any);
878
+ const result = await parseSolTx(vTx, mockConnection);
879
+
880
+ expect(result).toBeDefined();
881
+ expect(result!.from).toBe("FromV");
882
+ expect(result!.to).toBe("ToV");
883
+ expect(result!.amount).toBe(2000);
884
+ });
885
+
886
+ it("should return parsed data from versioned tx when decompile yields system transfer", async () => {
887
+ const { TransactionMessage } = await import("@solana/web3.js");
888
+ vi.mocked(TransactionMessage.decompile).mockReturnValue({
889
+ instructions: [
890
+ {
891
+ programId: { toString: () => "11111111111111111111111111111111" },
892
+ keys: [
893
+ { pubkey: { toString: () => "FromSys" } },
894
+ { pubkey: { toString: () => "ToSys" } },
895
+ ],
896
+ data: (() => {
897
+ const b = Buffer.alloc(12);
898
+ b[0] = 2;
899
+ b.writeBigUInt64LE(BigInt(2 * 1e9), 4);
900
+ return b;
901
+ })(),
902
+ },
903
+ ],
904
+ } as any);
905
+
906
+ const vTx = new VersionedTransaction({} as any);
907
+ const result = await parseSolTx(vTx, mockConnection);
908
+
909
+ expect(result).toBeDefined();
910
+ expect(result!.from).toBe("FromSys");
911
+ expect(result!.to).toBe("ToSys");
912
+ expect(result!.amount).toBe(2);
913
+ });
914
+
915
+ it("should return null when versioned parse returns no from", async () => {
916
+ const { TransactionMessage } = await import("@solana/web3.js");
917
+ vi.mocked(TransactionMessage.decompile).mockReturnValue({ instructions: [] } as any);
918
+
919
+ const vTx = new VersionedTransaction({} as any);
920
+ const result = await parseSolTx(vTx, mockConnection);
921
+
922
+ expect(result).toBeNull();
923
+ });
924
+
925
+ it("should return null when versioned decompile throws (parseSolVersionedTx catches and returns {})", async () => {
926
+ const { TransactionMessage } = await import("@solana/web3.js");
927
+ vi.mocked(TransactionMessage.decompile).mockImplementation(() => {
928
+ throw new Error("decompile failed");
929
+ });
930
+
931
+ const vTx = new VersionedTransaction({} as any);
932
+ const result = await parseSolTx(vTx, mockConnection);
933
+
934
+ expect(result).toBeNull();
935
+ });
936
+ });
937
+ });