@ledgerhq/coin-ton 0.8.2-nightly.0 → 0.8.2-nightly.2

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.
@@ -1,9 +1,16 @@
1
1
  import { encodeOperationId } from "@ledgerhq/coin-framework/lib/operation";
2
2
  import BigNumber from "bignumber.js";
3
3
  // eslint-disable-next-line no-restricted-imports
4
- import { flatMap } from "lodash";
4
+ import { Builder, Slice } from "@ton/core";
5
+ import flatMap from "lodash/flatMap";
5
6
  import { TonJettonTransfer, TonTransaction } from "../../bridge/bridgeHelpers/api.types";
6
- import { mapJettonTxToOps, mapTxToOps } from "../../bridge/bridgeHelpers/txn";
7
+ import {
8
+ dataToSlice,
9
+ decodeForwardPayload,
10
+ loadSnakeBytes,
11
+ mapJettonTxToOps,
12
+ mapTxToOps,
13
+ } from "../../bridge/bridgeHelpers/txn";
7
14
  import {
8
15
  jettonTransferResponse,
9
16
  mockAccountId,
@@ -13,7 +20,7 @@ import {
13
20
 
14
21
  describe("Transaction functions", () => {
15
22
  describe("mapTxToOps", () => {
16
- it.skip("should map an IN ton transaction without total_fees to a ledger operation", async () => {
23
+ it("should map an IN failed ton transaction without total_fees to a ledger operation", async () => {
17
24
  const { now, lt, hash, in_msg, total_fees, mc_block_seqno } =
18
25
  tonTransactionResponse.transactions[0];
19
26
 
@@ -30,20 +37,21 @@ describe("Transaction functions", () => {
30
37
  date: new Date(now * 1000), // now is defined in seconds
31
38
  extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
32
39
  fee: BigNumber(total_fees),
33
- hasFailed: false,
40
+ hasFailed: true,
34
41
  hash: in_msg?.hash,
35
42
  id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "IN"),
36
43
  recipients: [in_msg?.destination],
37
44
  senders: ["EQCVnqqL0OOiZi2BQnjVGm-ZeUYgfUhHgAi-vn9F8-94HwrH"],
38
45
  type: "IN",
39
46
  value: BigNumber(in_msg?.value ?? 0),
47
+ subOperations: undefined,
40
48
  },
41
49
  ]);
42
50
  });
43
51
 
44
- it.skip("should map an IN ton transaction with total_fees to a ledger operation", async () => {
52
+ it("should map an IN ton transaction with total_fees to a ledger operation", async () => {
45
53
  const transactions = [{ ...tonTransactionResponse.transactions[0], total_fees: "15" }];
46
- const { now, lt, hash, in_msg, total_fees, mc_block_seqno, account } = transactions[0];
54
+ const { now, lt, hash, in_msg, total_fees, mc_block_seqno } = transactions[0];
47
55
 
48
56
  const finalOperation = flatMap(
49
57
  transactions,
@@ -51,21 +59,6 @@ describe("Transaction functions", () => {
51
59
  );
52
60
 
53
61
  expect(finalOperation).toEqual([
54
- {
55
- id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "NONE"),
56
- hash: in_msg?.hash,
57
- type: "NONE",
58
- value: BigNumber(total_fees),
59
- fee: BigNumber(0),
60
- blockHash: null,
61
- blockHeight: mc_block_seqno,
62
- hasFailed: false,
63
- accountId: mockAccountId,
64
- senders: [account],
65
- recipients: [],
66
- date: new Date(now * 1000), // now is defined in seconds
67
- extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
68
- },
69
62
  {
70
63
  accountId: mockAccountId,
71
64
  blockHash: null,
@@ -73,18 +66,35 @@ describe("Transaction functions", () => {
73
66
  date: new Date(now * 1000), // now is defined in seconds
74
67
  extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
75
68
  fee: BigNumber(total_fees),
76
- hasFailed: false,
69
+ hasFailed: true,
77
70
  hash: in_msg?.hash,
78
71
  id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "IN"),
79
72
  recipients: [in_msg?.destination],
80
73
  senders: ["EQCVnqqL0OOiZi2BQnjVGm-ZeUYgfUhHgAi-vn9F8-94HwrH"],
81
74
  type: "IN",
82
75
  value: BigNumber(in_msg?.value ?? 0),
76
+ subOperations: [
77
+ {
78
+ id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "NONE"),
79
+ hash: in_msg?.hash,
80
+ type: "NONE",
81
+ value: BigNumber(total_fees),
82
+ fee: BigNumber(0),
83
+ blockHeight: mc_block_seqno,
84
+ blockHash: null,
85
+ hasFailed: true,
86
+ accountId: mockAccountId,
87
+ senders: [mockAddress],
88
+ recipients: [],
89
+ date: new Date(now * 1000),
90
+ extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
91
+ },
92
+ ],
83
93
  },
84
94
  ]);
85
95
  });
86
96
 
87
- it.skip("should map an OUT ton transaction to a ledger operation", async () => {
97
+ it("should map a failed OUT ton transaction to a ledger operation", async () => {
88
98
  // The IN transaction will be used as OUT transaction and it will be adjusted
89
99
  const transactions: TonTransaction[] = [
90
100
  {
@@ -97,7 +107,7 @@ describe("Transaction functions", () => {
97
107
  { ...tonTransactionResponse.transactions[0].in_msg, source: transactions[0].account },
98
108
  ];
99
109
  }
100
- const { now, lt, hash, out_msgs, total_fees, mc_block_seqno, account } = transactions[0];
110
+ const { now, lt, hash, out_msgs, total_fees, mc_block_seqno } = transactions[0];
101
111
 
102
112
  const finalOperation = flatMap(
103
113
  transactions,
@@ -106,16 +116,16 @@ describe("Transaction functions", () => {
106
116
 
107
117
  expect(finalOperation).toEqual([
108
118
  {
109
- id: encodeOperationId(mockAccountId, hash ?? "", "OUT"),
119
+ id: encodeOperationId(mockAccountId, hash, "OUT"),
110
120
  hash: out_msgs?.[0].hash,
111
121
  type: "OUT",
112
- value: BigNumber(out_msgs[0].value ?? 0).plus(BigNumber(total_fees)),
122
+ value: BigNumber(out_msgs?.[0].value ?? 0),
113
123
  fee: BigNumber(total_fees),
114
124
  blockHeight: mc_block_seqno,
115
125
  blockHash: null,
116
- hasFailed: false,
126
+ hasFailed: true,
117
127
  accountId: mockAccountId,
118
- senders: [account],
128
+ senders: [transactions[0].account],
119
129
  recipients: ["EQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbJfg"],
120
130
  date: new Date(now * 1000), // now is defined in seconds
121
131
  extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
@@ -200,3 +210,171 @@ describe("Transaction functions", () => {
200
210
  });
201
211
  });
202
212
  });
213
+
214
+ describe("TON Payload Processing Functions", () => {
215
+ describe("dataToSlice", () => {
216
+ it("should convert base64 string to Slice when it's a valid BOC", () => {
217
+ // Create a Cell from a string and convert to BOC
218
+ const cell = new Builder().storeUint(123, 32).endCell();
219
+ const bocBase64 = cell.toBoc().toString("base64");
220
+
221
+ const result = dataToSlice(bocBase64);
222
+
223
+ expect(result).toBeInstanceOf(Slice);
224
+ expect(result?.loadUint(32)).toBe(123);
225
+ });
226
+
227
+ it("should fallback to BitString when the data is not a valid BOC", () => {
228
+ const invalidBocBase64 = "aW52YWxpZCB0b24gZGF0YQ=="; // "invalid ton data"
229
+
230
+ const result = dataToSlice(invalidBocBase64);
231
+
232
+ expect(result).toBeInstanceOf(Slice);
233
+ });
234
+
235
+ it("should return undefined for non-string input", () => {
236
+ // @ts-expect-error - Testing invalid input
237
+ const result = dataToSlice(null);
238
+
239
+ expect(result).toBeUndefined();
240
+ });
241
+ });
242
+
243
+ describe("loadSnakeBytes", () => {
244
+ it("should load bytes from a simple slice without refs", () => {
245
+ const cell = new Builder().storeBuffer(Buffer.from("Slice", "utf-8")).endCell();
246
+ const slice = cell.beginParse();
247
+
248
+ const result = loadSnakeBytes(slice);
249
+
250
+ expect(result.toString("utf-8")).toBe("Slice");
251
+ });
252
+
253
+ it("should load bytes from a slice with refs (snake structure)", () => {
254
+ // Create a chain of cells (snake structure)
255
+ const cell2 = new Builder().storeBuffer(Buffer.from(" Data", "utf-8")).endCell();
256
+ const cell1 = new Builder()
257
+ .storeBuffer(Buffer.from("Slice", "utf-8"))
258
+ .storeRef(cell2)
259
+ .endCell();
260
+
261
+ const slice = cell1.beginParse();
262
+
263
+ const result = loadSnakeBytes(slice);
264
+
265
+ expect(result.toString("utf-8")).toBe("Slice Data");
266
+ });
267
+
268
+ it("should handle empty slice", () => {
269
+ const cell = new Builder().endCell();
270
+ const slice = cell.beginParse();
271
+
272
+ const result = loadSnakeBytes(slice);
273
+
274
+ expect(result.length).toBe(0);
275
+ });
276
+
277
+ it("should handle slice with multiple refs in chain", () => {
278
+ // Create a longer chain of cells (snake structure)
279
+ const cell3 = new Builder().storeBuffer(Buffer.from("Part3", "utf-8")).endCell();
280
+ const cell2 = new Builder()
281
+ .storeBuffer(Buffer.from("Part2", "utf-8"))
282
+ .storeRef(cell3)
283
+ .endCell();
284
+ const cell1 = new Builder()
285
+ .storeBuffer(Buffer.from("Part1", "utf-8"))
286
+ .storeRef(cell2)
287
+ .endCell();
288
+
289
+ const slice = cell1.beginParse();
290
+
291
+ const result = loadSnakeBytes(slice);
292
+
293
+ expect(result.toString("utf-8")).toBe("Part1Part2Part3");
294
+ });
295
+ });
296
+
297
+ describe("decodeForwardPayload", () => {
298
+ it("should return empty string for null payload", () => {
299
+ const result = decodeForwardPayload(null);
300
+
301
+ expect(result).toBe("");
302
+ });
303
+
304
+ it("should decode a valid payload with opcode 0 containing text", () => {
305
+ // Create a cell with opcode 0 followed by a text string
306
+ const cell = new Builder()
307
+ .storeUint(0, 32) // opcode 0
308
+ .storeBuffer(Buffer.from("This is the comment", "utf-8"))
309
+ .endCell();
310
+ const bocBase64 = cell.toBoc().toString("base64");
311
+
312
+ const result = decodeForwardPayload(bocBase64);
313
+
314
+ expect(result).toBe("This is the comment");
315
+ });
316
+
317
+ it("should return empty string for payloads with non-zero opcode", () => {
318
+ // Create a cell with opcode 1 followed by some data
319
+ const cell = new Builder()
320
+ .storeUint(1, 32) // non-zero opcode
321
+ .storeBuffer(Buffer.from("Should be ignored", "utf-8"))
322
+ .endCell();
323
+ const bocBase64 = cell.toBoc().toString("base64");
324
+
325
+ const result = decodeForwardPayload(bocBase64);
326
+
327
+ expect(result).toBe("");
328
+ });
329
+
330
+ it("should handle payload with unicode characters", () => {
331
+ // Create a cell with opcode 0 followed by a text with unicode
332
+ const cell = new Builder()
333
+ .storeUint(0, 32) // opcode 0
334
+ .storeBuffer(Buffer.from("Unicode: 你好, мир, 🚀", "utf-8"))
335
+ .endCell();
336
+ const bocBase64 = cell.toBoc().toString("base64");
337
+
338
+ const result = decodeForwardPayload(bocBase64);
339
+
340
+ expect(result).toBe("Unicode: 你好, мир, 🚀");
341
+ });
342
+
343
+ it("should handle snake format payloads correctly", () => {
344
+ // Create a chain of cells with opcode 0 followed by a long message
345
+ const cell2 = new Builder()
346
+ .storeBuffer(Buffer.from(" would need multiple cells to store.", "utf-8"))
347
+ .endCell();
348
+ const cell1 = new Builder()
349
+ .storeUint(0, 32) // opcode 0
350
+ .storeBuffer(Buffer.from("This is a very long message that", "utf-8"))
351
+ .storeRef(cell2)
352
+ .endCell();
353
+
354
+ const bocBase64 = cell1.toBoc().toString("base64");
355
+
356
+ const result = decodeForwardPayload(bocBase64);
357
+
358
+ expect(result).toBe("This is a very long message that would need multiple cells to store.");
359
+ });
360
+
361
+ it("should handle invalid payloads gracefully by returning empty string", () => {
362
+ // Create an invalid base64 string
363
+ const invalidBase64 = "!@#$%^&*()";
364
+
365
+ const result = decodeForwardPayload(invalidBase64);
366
+
367
+ expect(result).toBe("");
368
+ });
369
+
370
+ it("should handle valid base64 but invalid BOC payloads", () => {
371
+ // Valid base64 but not a valid BOC
372
+ const validBase64NotBoc = "aW52YWxpZCB0b24gZGF0YQ=="; // "invalid ton data" in base64
373
+
374
+ const result = decodeForwardPayload(validBase64NotBoc);
375
+
376
+ // Should return empty string as it's not a valid Cell
377
+ expect(result).toBe("");
378
+ });
379
+ });
380
+ });
@@ -2,7 +2,7 @@ import { decodeAccountId, encodeTokenAccountId } from "@ledgerhq/coin-framework/
2
2
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
3
3
  import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets/tokens";
4
4
  import { Operation } from "@ledgerhq/types-live";
5
- import { Address, Cell } from "@ton/core";
5
+ import { Address, BitReader, BitString, Cell, Slice } from "@ton/core";
6
6
  import BigNumber from "bignumber.js";
7
7
  import { TonOperation } from "../../types";
8
8
  import { addressesAreEqual, isAddressValid } from "../../utils";
@@ -281,25 +281,61 @@ export function mapJettonTxToOps(
281
281
  },
282
282
  });
283
283
  }
284
-
285
284
  return ops;
286
285
  };
287
286
  }
288
287
 
289
- function decodeForwardPayload(payload: string | null): string {
288
+ // Export these functions for testing
289
+ export function dataToSlice(data: string): Slice | undefined {
290
+ let buffer: Buffer;
291
+ if (typeof data === "string") {
292
+ buffer = Buffer.from(data, "base64");
293
+
294
+ try {
295
+ return Cell.fromBoc(buffer)[0].beginParse();
296
+ } catch (err) {
297
+ return new Slice(new BitReader(new BitString(buffer, 0, buffer.length * 8)), []);
298
+ }
299
+ }
300
+
301
+ return undefined;
302
+ }
303
+
304
+ export function loadSnakeBytes(slice: Slice): Buffer {
305
+ let buffer = Buffer.alloc(0);
306
+
307
+ while (slice.remainingBits >= 8) {
308
+ buffer = Buffer.concat([buffer, slice.loadBuffer(slice.remainingBits / 8)]);
309
+ if (slice.remainingRefs) {
310
+ slice = slice.loadRef().beginParse();
311
+ } else {
312
+ break;
313
+ }
314
+ }
315
+
316
+ return buffer;
317
+ }
318
+
319
+ export function decodeForwardPayload(payload: string | null): string {
290
320
  if (!payload) return "";
291
321
 
292
- const decodedBuffer = Buffer.from(payload, "base64");
293
- const cell = Cell.fromBoc(decodedBuffer)[0];
294
- const slice = cell.beginParse();
322
+ try {
323
+ const slice = dataToSlice(payload);
324
+
325
+ if (!slice) return "";
295
326
 
296
- // Read the opcode
297
- const opcode = slice.loadUint(32);
298
- if (opcode !== 0) {
327
+ const opcode = slice.loadUint(32);
328
+
329
+ // Format with opcode 0 followed by text
330
+ if (opcode !== 0) {
331
+ return "";
332
+ }
333
+ const buffer = loadSnakeBytes(slice);
334
+ const comment = buffer.toString("utf-8");
335
+
336
+ return comment;
337
+ } catch (error) {
338
+ // Silent failure, returning empty string
299
339
  return "";
300
340
  }
301
-
302
- // Read the comment
303
- const comment = slice.loadStringTail();
304
- return comment;
305
341
  }