@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +13 -0
- package/lib/__tests__/unit/txn.unit.test.js +161 -32
- package/lib/__tests__/unit/txn.unit.test.js.map +1 -1
- package/lib/bridge/bridgeHelpers/txn.d.ts +6 -0
- package/lib/bridge/bridgeHelpers/txn.d.ts.map +1 -1
- package/lib/bridge/bridgeHelpers/txn.js +46 -10
- package/lib/bridge/bridgeHelpers/txn.js.map +1 -1
- package/lib-es/__tests__/unit/txn.unit.test.js +157 -28
- package/lib-es/__tests__/unit/txn.unit.test.js.map +1 -1
- package/lib-es/bridge/bridgeHelpers/txn.d.ts +6 -0
- package/lib-es/bridge/bridgeHelpers/txn.d.ts.map +1 -1
- package/lib-es/bridge/bridgeHelpers/txn.js +44 -11
- package/lib-es/bridge/bridgeHelpers/txn.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/unit/txn.unit.test.ts +206 -28
- package/src/bridge/bridgeHelpers/txn.ts +49 -13
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
322
|
+
try {
|
|
323
|
+
const slice = dataToSlice(payload);
|
|
324
|
+
|
|
325
|
+
if (!slice) return "";
|
|
295
326
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
}
|