@ledgerhq/coin-filecoin 1.14.1-nightly.20251113102200 → 1.15.0-nightly.20251115023630
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/.unimportedrc.json +2 -0
- package/CHANGELOG.md +11 -7
- package/lib/api/api.d.ts +5 -3
- package/lib/api/api.d.ts.map +1 -1
- package/lib/api/api.js +66 -21
- package/lib/api/api.js.map +1 -1
- package/lib/common-logic/index.d.ts +1 -1
- package/lib/common-logic/index.d.ts.map +1 -1
- package/lib/common-logic/index.js +1 -3
- package/lib/common-logic/index.js.map +1 -1
- package/lib/common-logic/utils.d.ts +0 -2
- package/lib/common-logic/utils.d.ts.map +1 -1
- package/lib/common-logic/utils.js +16 -68
- package/lib/common-logic/utils.js.map +1 -1
- package/lib/erc20/tokenAccounts.d.ts +2 -3
- package/lib/erc20/tokenAccounts.d.ts.map +1 -1
- package/lib/erc20/tokenAccounts.js +46 -32
- package/lib/erc20/tokenAccounts.js.map +1 -1
- package/lib/test/fixtures.d.ts +87 -0
- package/lib/test/fixtures.d.ts.map +1 -0
- package/lib/test/fixtures.js +311 -0
- package/lib/test/fixtures.js.map +1 -0
- package/lib/types/common.d.ts +22 -2
- package/lib/types/common.d.ts.map +1 -1
- package/lib-es/api/api.d.ts +5 -3
- package/lib-es/api/api.d.ts.map +1 -1
- package/lib-es/api/api.js +61 -18
- package/lib-es/api/api.js.map +1 -1
- package/lib-es/common-logic/index.d.ts +1 -1
- package/lib-es/common-logic/index.d.ts.map +1 -1
- package/lib-es/common-logic/index.js +1 -1
- package/lib-es/common-logic/index.js.map +1 -1
- package/lib-es/common-logic/utils.d.ts +0 -2
- package/lib-es/common-logic/utils.d.ts.map +1 -1
- package/lib-es/common-logic/utils.js +16 -66
- package/lib-es/common-logic/utils.js.map +1 -1
- package/lib-es/erc20/tokenAccounts.d.ts +2 -3
- package/lib-es/erc20/tokenAccounts.d.ts.map +1 -1
- package/lib-es/erc20/tokenAccounts.js +47 -33
- package/lib-es/erc20/tokenAccounts.js.map +1 -1
- package/lib-es/test/fixtures.d.ts +87 -0
- package/lib-es/test/fixtures.d.ts.map +1 -0
- package/lib-es/test/fixtures.js +297 -0
- package/lib-es/test/fixtures.js.map +1 -0
- package/lib-es/types/common.d.ts +22 -2
- package/lib-es/types/common.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/api/api.ts +107 -26
- package/src/api/api.unit.test.ts +217 -0
- package/src/common-logic/index.ts +0 -2
- package/src/common-logic/utils.ts +19 -90
- package/src/common-logic/utils.unit.test.ts +429 -0
- package/src/erc20/tokenAccounts.ts +59 -34
- package/src/erc20/tokenAccounts.unit.test.ts +73 -0
- package/src/test/fixtures.ts +342 -0
- package/src/types/common.ts +24 -2
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mapTxToOps,
|
|
3
|
+
getAddress,
|
|
4
|
+
getTxToBroadcast,
|
|
5
|
+
getAccountShape,
|
|
6
|
+
getSubAccount,
|
|
7
|
+
valueFromUnit,
|
|
8
|
+
} from "./utils";
|
|
9
|
+
import {
|
|
10
|
+
createMockAccount,
|
|
11
|
+
createMockTransactionResponse,
|
|
12
|
+
createMockOperation,
|
|
13
|
+
createMockTransaction,
|
|
14
|
+
TEST_ADDRESSES,
|
|
15
|
+
createMockBalanceResponse,
|
|
16
|
+
createMockTokenAccount,
|
|
17
|
+
TEST_BLOCK_HEIGHTS,
|
|
18
|
+
} from "../test/fixtures";
|
|
19
|
+
import { TxStatus } from "../types";
|
|
20
|
+
import BigNumber from "bignumber.js";
|
|
21
|
+
import { DerivationMode } from "@ledgerhq/types-live";
|
|
22
|
+
import * as api from "../api/api";
|
|
23
|
+
import * as tokenAccounts from "../erc20/tokenAccounts";
|
|
24
|
+
|
|
25
|
+
// Mock API and token account modules
|
|
26
|
+
jest.mock("../api/api");
|
|
27
|
+
jest.mock("../erc20/tokenAccounts");
|
|
28
|
+
|
|
29
|
+
describe("common-logic/utils", () => {
|
|
30
|
+
describe("mapTxToOps", () => {
|
|
31
|
+
const createAccountShapeInfo = (address: string) => ({
|
|
32
|
+
address,
|
|
33
|
+
currency: createMockAccount().currency,
|
|
34
|
+
index: 0,
|
|
35
|
+
derivationPath: "44'/461'/0'/0/0",
|
|
36
|
+
derivationMode: "" as DerivationMode,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should convert send transaction to OUT operation", () => {
|
|
40
|
+
const account = createMockAccount({
|
|
41
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const tx = createMockTransactionResponse({
|
|
45
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
46
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
47
|
+
amount: "100000000000000000",
|
|
48
|
+
status: TxStatus.Ok,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const mapper = mapTxToOps(account.id, createAccountShapeInfo(TEST_ADDRESSES.F1_ADDRESS));
|
|
52
|
+
const ops = mapper(tx);
|
|
53
|
+
|
|
54
|
+
expect(ops).toHaveLength(1);
|
|
55
|
+
expect(ops[0].type).toBe("OUT");
|
|
56
|
+
expect(ops[0].value.gt(new BigNumber("100000000000000000"))).toBe(true); // includes fees
|
|
57
|
+
expect(ops[0].hasFailed).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should convert receive transaction to IN operation", () => {
|
|
61
|
+
const account = createMockAccount({
|
|
62
|
+
freshAddress: TEST_ADDRESSES.RECIPIENT_F1,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const tx = createMockTransactionResponse({
|
|
66
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
67
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
68
|
+
amount: "100000000000000000",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const mapper = mapTxToOps(account.id, createAccountShapeInfo(TEST_ADDRESSES.RECIPIENT_F1));
|
|
72
|
+
const ops = mapper(tx);
|
|
73
|
+
|
|
74
|
+
expect(ops).toHaveLength(1);
|
|
75
|
+
expect(ops[0].type).toBe("IN");
|
|
76
|
+
expect(ops[0].value.isEqualTo(new BigNumber("100000000000000000"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle zero amount transaction as FEES", () => {
|
|
80
|
+
const account = createMockAccount({
|
|
81
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const tx = createMockTransactionResponse({
|
|
85
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
86
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
87
|
+
amount: "0",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const mapper = mapTxToOps(account.id, createAccountShapeInfo(TEST_ADDRESSES.F1_ADDRESS));
|
|
91
|
+
const ops = mapper(tx);
|
|
92
|
+
|
|
93
|
+
expect(ops).toHaveLength(1);
|
|
94
|
+
expect(ops[0].type).toBe("FEES");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should mark failed transactions", () => {
|
|
98
|
+
const account = createMockAccount({
|
|
99
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const tx = createMockTransactionResponse({
|
|
103
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
104
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
105
|
+
status: "Fail",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const mapper = mapTxToOps(account.id, createAccountShapeInfo(TEST_ADDRESSES.F1_ADDRESS));
|
|
109
|
+
const ops = mapper(tx);
|
|
110
|
+
|
|
111
|
+
expect(ops[0].hasFailed).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should handle self-transfer (both send and receive)", () => {
|
|
115
|
+
const account = createMockAccount({
|
|
116
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const tx = createMockTransactionResponse({
|
|
120
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
121
|
+
to: TEST_ADDRESSES.F1_ADDRESS,
|
|
122
|
+
amount: "100000000000000000",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const mapper = mapTxToOps(account.id, createAccountShapeInfo(TEST_ADDRESSES.F1_ADDRESS));
|
|
126
|
+
const ops = mapper(tx);
|
|
127
|
+
|
|
128
|
+
expect(ops).toHaveLength(2);
|
|
129
|
+
expect(ops.some(op => op.type === "OUT")).toBe(true);
|
|
130
|
+
expect(ops.some(op => op.type === "IN")).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("getAddress", () => {
|
|
135
|
+
it("should extract address and derivation path from account", () => {
|
|
136
|
+
const account = createMockAccount({
|
|
137
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
138
|
+
freshAddressPath: "44'/461'/0'/0/0",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = getAddress(account);
|
|
142
|
+
|
|
143
|
+
expect(result.address).toBe(TEST_ADDRESSES.F1_ADDRESS);
|
|
144
|
+
expect(result.derivationPath).toBe("44'/461'/0'/0/0");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("getTxToBroadcast", () => {
|
|
149
|
+
it("should format transaction for broadcasting", () => {
|
|
150
|
+
const account = createMockAccount();
|
|
151
|
+
const transaction = createMockTransaction();
|
|
152
|
+
const operation = createMockOperation(account, transaction);
|
|
153
|
+
|
|
154
|
+
const rawData = {
|
|
155
|
+
sender: TEST_ADDRESSES.F1_ADDRESS,
|
|
156
|
+
recipient: TEST_ADDRESSES.RECIPIENT_F1,
|
|
157
|
+
gasLimit: new BigNumber(1000000),
|
|
158
|
+
gasFeeCap: new BigNumber("100000"),
|
|
159
|
+
gasPremium: new BigNumber("100000"),
|
|
160
|
+
method: 0,
|
|
161
|
+
version: 0,
|
|
162
|
+
nonce: 5,
|
|
163
|
+
signatureType: 1,
|
|
164
|
+
params: "",
|
|
165
|
+
value: "100000000000000000",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = getTxToBroadcast(operation, "signature_data", rawData);
|
|
169
|
+
|
|
170
|
+
expect(result.message.from).toBe(TEST_ADDRESSES.F1_ADDRESS);
|
|
171
|
+
expect(result.message.to).toBe(TEST_ADDRESSES.RECIPIENT_F1);
|
|
172
|
+
expect(result.message.gaslimit).toBe(1000000);
|
|
173
|
+
expect(result.message.gasfeecap).toBe("100000");
|
|
174
|
+
expect(result.message.gaspremium).toBe("100000");
|
|
175
|
+
expect(result.message.nonce).toBe(5);
|
|
176
|
+
expect(result.signature.type).toBe(1);
|
|
177
|
+
expect(result.signature.data).toBe("signature_data");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle ERC20 contract calls with params", () => {
|
|
181
|
+
const operation = createMockOperation(createMockAccount(), createMockTransaction());
|
|
182
|
+
|
|
183
|
+
const rawData = {
|
|
184
|
+
sender: TEST_ADDRESSES.F4_ADDRESS,
|
|
185
|
+
recipient: TEST_ADDRESSES.ERC20_CONTRACT,
|
|
186
|
+
gasLimit: new BigNumber(2000000),
|
|
187
|
+
gasFeeCap: new BigNumber("200000"),
|
|
188
|
+
gasPremium: new BigNumber("150000"),
|
|
189
|
+
method: 3844450837,
|
|
190
|
+
version: 0,
|
|
191
|
+
nonce: 10,
|
|
192
|
+
signatureType: 1,
|
|
193
|
+
params: "base64encodedparams",
|
|
194
|
+
value: "0",
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = getTxToBroadcast(operation, "sig", rawData);
|
|
198
|
+
|
|
199
|
+
expect(result.message.params).toBe("base64encodedparams");
|
|
200
|
+
expect(result.message.method).toBe(3844450837);
|
|
201
|
+
expect(result.message.value).toBe("0");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("getAccountShape", () => {
|
|
206
|
+
const mockedFetchBlockHeight = api.fetchBlockHeight as jest.MockedFunction<
|
|
207
|
+
typeof api.fetchBlockHeight
|
|
208
|
+
>;
|
|
209
|
+
const mockedFetchBalances = api.fetchBalances as jest.MockedFunction<typeof api.fetchBalances>;
|
|
210
|
+
const mockedFetchTxsWithPages = api.fetchTxsWithPages as jest.MockedFunction<
|
|
211
|
+
typeof api.fetchTxsWithPages
|
|
212
|
+
>;
|
|
213
|
+
const mockedBuildTokenAccounts = tokenAccounts.buildTokenAccounts as jest.MockedFunction<
|
|
214
|
+
typeof tokenAccounts.buildTokenAccounts
|
|
215
|
+
>;
|
|
216
|
+
|
|
217
|
+
const mockSyncConfig = {
|
|
218
|
+
paginationConfig: {},
|
|
219
|
+
blacklistedTokenIds: [],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
jest.clearAllMocks();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should fetch and build account shape with balances and token accounts", async () => {
|
|
227
|
+
const mockAccount = createMockAccount({
|
|
228
|
+
freshAddress: TEST_ADDRESSES.F1_ADDRESS,
|
|
229
|
+
blockHeight: TEST_BLOCK_HEIGHTS.CURRENT,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const mockBalance = createMockBalanceResponse({
|
|
233
|
+
total_balance: "1000000000000000000",
|
|
234
|
+
spendable_balance: "900000000000000000",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const mockBlockHeight = {
|
|
238
|
+
current_block_identifier: {
|
|
239
|
+
index: TEST_BLOCK_HEIGHTS.CURRENT,
|
|
240
|
+
hash: "current_block_hash",
|
|
241
|
+
},
|
|
242
|
+
genesis_block_identifier: {
|
|
243
|
+
index: 0,
|
|
244
|
+
hash: "genesis",
|
|
245
|
+
},
|
|
246
|
+
current_block_timestamp: Date.now(),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const mockTxs = [
|
|
250
|
+
createMockTransactionResponse({
|
|
251
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
252
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
253
|
+
}),
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const mockParentAccount = createMockAccount();
|
|
257
|
+
const mockTokenAccounts = [
|
|
258
|
+
createMockTokenAccount(mockParentAccount, {
|
|
259
|
+
balance: new BigNumber("5000000000000000000"),
|
|
260
|
+
}),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
mockedFetchBlockHeight.mockResolvedValue(mockBlockHeight);
|
|
264
|
+
mockedFetchBalances.mockResolvedValue(mockBalance);
|
|
265
|
+
mockedFetchTxsWithPages.mockResolvedValue(mockTxs);
|
|
266
|
+
mockedBuildTokenAccounts.mockResolvedValue(mockTokenAccounts);
|
|
267
|
+
|
|
268
|
+
const info = {
|
|
269
|
+
address: TEST_ADDRESSES.F1_ADDRESS,
|
|
270
|
+
currency: mockAccount.currency,
|
|
271
|
+
derivationMode: "" as DerivationMode,
|
|
272
|
+
initialAccount: mockAccount,
|
|
273
|
+
index: 0,
|
|
274
|
+
derivationPath: "44'/461'/0'/0/0",
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const result = await getAccountShape(info, mockSyncConfig);
|
|
278
|
+
|
|
279
|
+
expect(result.balance?.isEqualTo(new BigNumber("1000000000000000000"))).toBe(true);
|
|
280
|
+
expect(result.spendableBalance?.isEqualTo(new BigNumber("900000000000000000"))).toBe(true);
|
|
281
|
+
expect(result.blockHeight).toBe(TEST_BLOCK_HEIGHTS.CURRENT);
|
|
282
|
+
expect(result.subAccounts).toEqual(mockTokenAccounts);
|
|
283
|
+
expect(result.operations).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should handle block height safe delta correctly", async () => {
|
|
287
|
+
const mockAccount = createMockAccount({
|
|
288
|
+
blockHeight: 500, // Less than blockSafeDelta (1200)
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const mockBalance = createMockBalanceResponse();
|
|
292
|
+
const mockBlockHeight = {
|
|
293
|
+
current_block_identifier: {
|
|
294
|
+
index: TEST_BLOCK_HEIGHTS.CURRENT,
|
|
295
|
+
hash: "hash",
|
|
296
|
+
},
|
|
297
|
+
genesis_block_identifier: {
|
|
298
|
+
index: 0,
|
|
299
|
+
hash: "genesis",
|
|
300
|
+
},
|
|
301
|
+
current_block_timestamp: Date.now(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
mockedFetchBlockHeight.mockResolvedValue(mockBlockHeight);
|
|
305
|
+
mockedFetchBalances.mockResolvedValue(mockBalance);
|
|
306
|
+
mockedFetchTxsWithPages.mockResolvedValue([]);
|
|
307
|
+
mockedBuildTokenAccounts.mockResolvedValue([]);
|
|
308
|
+
|
|
309
|
+
const info = {
|
|
310
|
+
address: TEST_ADDRESSES.F1_ADDRESS,
|
|
311
|
+
currency: mockAccount.currency,
|
|
312
|
+
derivationMode: "" as DerivationMode,
|
|
313
|
+
initialAccount: mockAccount,
|
|
314
|
+
index: 0,
|
|
315
|
+
derivationPath: "44'/461'/0'/0/0",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
await getAccountShape(info, mockSyncConfig);
|
|
319
|
+
|
|
320
|
+
// Should call fetchTxsWithPages with lastHeight = 0 (not negative)
|
|
321
|
+
expect(mockedFetchTxsWithPages).toHaveBeenCalledWith(TEST_ADDRESSES.F1_ADDRESS, 0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should sort operations by date descending", async () => {
|
|
325
|
+
const now = Math.floor(Date.now() / 1000);
|
|
326
|
+
const mockBalance = createMockBalanceResponse();
|
|
327
|
+
const mockBlockHeight = {
|
|
328
|
+
current_block_identifier: {
|
|
329
|
+
index: TEST_BLOCK_HEIGHTS.CURRENT,
|
|
330
|
+
hash: "hash",
|
|
331
|
+
},
|
|
332
|
+
genesis_block_identifier: {
|
|
333
|
+
index: 0,
|
|
334
|
+
hash: "genesis",
|
|
335
|
+
},
|
|
336
|
+
current_block_timestamp: Date.now(),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const mockTxs = [
|
|
340
|
+
createMockTransactionResponse({
|
|
341
|
+
hash: "tx1",
|
|
342
|
+
timestamp: now - 1000,
|
|
343
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
344
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
345
|
+
}),
|
|
346
|
+
createMockTransactionResponse({
|
|
347
|
+
hash: "tx2",
|
|
348
|
+
timestamp: now - 500,
|
|
349
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
350
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
351
|
+
}),
|
|
352
|
+
createMockTransactionResponse({
|
|
353
|
+
hash: "tx3",
|
|
354
|
+
timestamp: now - 2000,
|
|
355
|
+
from: TEST_ADDRESSES.F1_ADDRESS,
|
|
356
|
+
to: TEST_ADDRESSES.RECIPIENT_F1,
|
|
357
|
+
}),
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
mockedFetchBlockHeight.mockResolvedValue(mockBlockHeight);
|
|
361
|
+
mockedFetchBalances.mockResolvedValue(mockBalance);
|
|
362
|
+
mockedFetchTxsWithPages.mockResolvedValue(mockTxs);
|
|
363
|
+
mockedBuildTokenAccounts.mockResolvedValue([]);
|
|
364
|
+
|
|
365
|
+
const info = {
|
|
366
|
+
address: TEST_ADDRESSES.F1_ADDRESS,
|
|
367
|
+
currency: createMockAccount().currency,
|
|
368
|
+
derivationMode: "" as DerivationMode,
|
|
369
|
+
initialAccount: undefined,
|
|
370
|
+
index: 0,
|
|
371
|
+
derivationPath: "44'/461'/0'/0/0",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = await getAccountShape(info, mockSyncConfig);
|
|
375
|
+
|
|
376
|
+
// Operations should be sorted newest first
|
|
377
|
+
expect(result.operations?.[0].hash).toBe("tx2");
|
|
378
|
+
expect(result.operations?.[1].hash).toBe("tx1");
|
|
379
|
+
expect(result.operations?.[2].hash).toBe("tx3");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("getSubAccount", () => {
|
|
384
|
+
it("should return sub account when transaction has subAccountId", () => {
|
|
385
|
+
const account = createMockAccount();
|
|
386
|
+
const subAccount = createMockTokenAccount(account, {
|
|
387
|
+
id: "subaccount123",
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const accountWithSub = createMockAccount({
|
|
391
|
+
subAccounts: [subAccount],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const transaction = createMockTransaction({
|
|
395
|
+
subAccountId: "subaccount123",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const result = getSubAccount(accountWithSub, transaction);
|
|
399
|
+
|
|
400
|
+
expect(result).toEqual(subAccount);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("valueFromUnit", () => {
|
|
405
|
+
it("should convert value with unit magnitude", () => {
|
|
406
|
+
const unit = {
|
|
407
|
+
name: "FIL",
|
|
408
|
+
code: "FIL",
|
|
409
|
+
magnitude: 18,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const result = valueFromUnit(new BigNumber(1), unit);
|
|
413
|
+
|
|
414
|
+
expect(result.isEqualTo(new BigNumber("1000000000000000000"))).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should handle decimal values", () => {
|
|
418
|
+
const unit = {
|
|
419
|
+
name: "FIL",
|
|
420
|
+
code: "FIL",
|
|
421
|
+
magnitude: 18,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const result = valueFromUnit(new BigNumber("0.5"), unit);
|
|
425
|
+
|
|
426
|
+
expect(result.isEqualTo(new BigNumber("500000000000000000"))).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cbor from "@zondax/cbor";
|
|
2
2
|
import { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
|
|
3
|
-
import { fetchERC20TokenBalance,
|
|
3
|
+
import { fetchERC20TokenBalance, fetchERC20TransactionsWithPages } from "../api";
|
|
4
4
|
import invariant from "invariant";
|
|
5
5
|
import { ERC20Transfer, TxStatus } from "../types";
|
|
6
6
|
import { emptyHistoryCache, encodeTokenAccountId } from "@ledgerhq/coin-framework/account/index";
|
|
@@ -12,19 +12,17 @@ import { convertAddressFilToEth } from "../network";
|
|
|
12
12
|
import { ethers } from "ethers";
|
|
13
13
|
import contractABI from "./ERC20.json";
|
|
14
14
|
import { RecipientRequired } from "@ledgerhq/errors";
|
|
15
|
-
import { Unit } from "@ledgerhq/types-cryptoassets";
|
|
16
15
|
import { AccountType } from "../bridge/utils";
|
|
17
|
-
import {
|
|
16
|
+
import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
18
17
|
|
|
19
18
|
export const erc20TxnToOperation = (
|
|
20
19
|
tx: ERC20Transfer,
|
|
21
20
|
address: string,
|
|
22
21
|
accountId: string,
|
|
23
|
-
unit: Unit,
|
|
24
22
|
): Operation[] => {
|
|
25
23
|
try {
|
|
26
24
|
const { to, from, timestamp, tx_hash, tx_cid, amount, height, status } = tx;
|
|
27
|
-
const
|
|
25
|
+
const txAmount = new BigNumber(amount);
|
|
28
26
|
|
|
29
27
|
const isSending = address.toLowerCase() === from.toLowerCase();
|
|
30
28
|
const isReceiving = address.toLowerCase() === to.toLowerCase();
|
|
@@ -41,7 +39,7 @@ export const erc20TxnToOperation = (
|
|
|
41
39
|
id: encodeOperationId(accountId, hash, "OUT"),
|
|
42
40
|
hash,
|
|
43
41
|
type: "OUT",
|
|
44
|
-
value:
|
|
42
|
+
value: txAmount,
|
|
45
43
|
fee,
|
|
46
44
|
blockHeight: height,
|
|
47
45
|
blockHash: "",
|
|
@@ -59,7 +57,7 @@ export const erc20TxnToOperation = (
|
|
|
59
57
|
id: encodeOperationId(accountId, hash, "IN"),
|
|
60
58
|
hash,
|
|
61
59
|
type: "IN",
|
|
62
|
-
value,
|
|
60
|
+
value: txAmount,
|
|
63
61
|
fee,
|
|
64
62
|
blockHeight: height,
|
|
65
63
|
blockHash: "",
|
|
@@ -83,51 +81,71 @@ export const erc20TxnToOperation = (
|
|
|
83
81
|
|
|
84
82
|
export async function buildTokenAccounts(
|
|
85
83
|
filAddr: string,
|
|
84
|
+
lastHeight: number,
|
|
86
85
|
parentAccountId: string,
|
|
87
86
|
initialAccount?: Account,
|
|
88
87
|
): Promise<TokenAccount[]> {
|
|
89
88
|
try {
|
|
90
|
-
const transfers = await
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
const transfers = await fetchERC20TransactionsWithPages(filAddr, lastHeight);
|
|
90
|
+
|
|
91
|
+
if (!transfers.length) {
|
|
92
|
+
return initialAccount?.subAccounts ?? [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Group transfers by contract address (normalized to lowercase)
|
|
96
|
+
const transfersByContract = transfers.reduce<Record<string, ERC20Transfer[]>>(
|
|
97
|
+
(acc, transfer) => {
|
|
98
|
+
const contractAddr = transfer.contract_address.toLowerCase();
|
|
99
|
+
transfer.contract_address = contractAddr;
|
|
100
|
+
|
|
101
|
+
if (!acc[contractAddr]) {
|
|
102
|
+
acc[contractAddr] = [];
|
|
98
103
|
}
|
|
99
|
-
|
|
104
|
+
acc[contractAddr].push(transfer);
|
|
105
|
+
return acc;
|
|
100
106
|
},
|
|
101
107
|
{},
|
|
102
108
|
);
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
// Create lookup map for existing sub-accounts
|
|
111
|
+
const existingSubAccounts = new Map(
|
|
112
|
+
initialAccount?.subAccounts?.map(sa => [sa.token.contractAddress.toLowerCase(), sa]) ?? [],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Track which existing accounts we've processed
|
|
116
|
+
const processedContracts = new Set<string>();
|
|
117
|
+
const tokenAccounts: TokenAccount[] = [];
|
|
118
|
+
|
|
119
|
+
// Process accounts with new transfers
|
|
120
|
+
for (const [contractAddr, txns] of Object.entries(transfersByContract)) {
|
|
121
|
+
processedContracts.add(contractAddr);
|
|
122
|
+
|
|
123
|
+
const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(
|
|
124
|
+
contractAddr,
|
|
125
|
+
"filecoin",
|
|
126
|
+
);
|
|
107
127
|
if (!token) {
|
|
108
|
-
log("error", `filecoin token not found, addr: ${
|
|
128
|
+
log("error", `filecoin token not found, addr: ${contractAddr}`);
|
|
109
129
|
continue;
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
const balance = await fetchERC20TokenBalance(filAddr,
|
|
113
|
-
const bnBalance = new BigNumber(balance
|
|
132
|
+
const balance = await fetchERC20TokenBalance(filAddr, contractAddr);
|
|
133
|
+
const bnBalance = new BigNumber(balance);
|
|
114
134
|
const tokenAccountId = encodeTokenAccountId(parentAccountId, token);
|
|
115
135
|
|
|
116
136
|
const operations = txns
|
|
117
|
-
.flatMap(txn => erc20TxnToOperation(txn, filAddr, tokenAccountId
|
|
137
|
+
.flatMap(txn => erc20TxnToOperation(txn, filAddr, tokenAccountId))
|
|
118
138
|
.flat()
|
|
119
139
|
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
120
140
|
|
|
141
|
+
// Skip if no operations and zero balance
|
|
121
142
|
if (operations.length === 0 && bnBalance.isZero()) {
|
|
122
143
|
continue;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
const
|
|
126
|
-
initialAccount &&
|
|
127
|
-
initialAccount.subAccounts &&
|
|
128
|
-
initialAccount.subAccounts.find(a => a.id === tokenAccountId);
|
|
146
|
+
const existingAccount = existingSubAccounts.get(contractAddr);
|
|
129
147
|
|
|
130
|
-
const
|
|
148
|
+
const tokenAccount: TokenAccount = {
|
|
131
149
|
type: AccountType.TokenAccount,
|
|
132
150
|
id: tokenAccountId,
|
|
133
151
|
parentId: parentAccountId,
|
|
@@ -135,17 +153,24 @@ export async function buildTokenAccounts(
|
|
|
135
153
|
balance: bnBalance,
|
|
136
154
|
spendableBalance: bnBalance,
|
|
137
155
|
operationsCount: txns.length,
|
|
138
|
-
operations,
|
|
139
|
-
pendingOperations:
|
|
140
|
-
creationDate: operations.length
|
|
141
|
-
swapHistory:
|
|
156
|
+
operations: mergeOps(existingAccount?.operations ?? [], operations),
|
|
157
|
+
pendingOperations: existingAccount?.pendingOperations ?? [],
|
|
158
|
+
creationDate: operations[operations.length - 1]?.date ?? new Date(),
|
|
159
|
+
swapHistory: existingAccount?.swapHistory ?? [],
|
|
142
160
|
balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers
|
|
143
161
|
};
|
|
144
162
|
|
|
145
|
-
|
|
163
|
+
tokenAccounts.push(tokenAccount);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Add existing accounts that didn't have new transfers
|
|
167
|
+
for (const [contractAddr, existingAccount] of existingSubAccounts) {
|
|
168
|
+
if (!processedContracts.has(contractAddr)) {
|
|
169
|
+
tokenAccounts.push(existingAccount);
|
|
170
|
+
}
|
|
146
171
|
}
|
|
147
172
|
|
|
148
|
-
return
|
|
173
|
+
return tokenAccounts;
|
|
149
174
|
} catch (e) {
|
|
150
175
|
log("error", "filecoin error building token accounts", e);
|
|
151
176
|
return [];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { erc20TxnToOperation } from "./tokenAccounts";
|
|
2
|
+
import { createMockERC20Transfer, TEST_ADDRESSES } from "../test/fixtures";
|
|
3
|
+
import BigNumber from "bignumber.js";
|
|
4
|
+
import { TxStatus } from "../types";
|
|
5
|
+
|
|
6
|
+
jest.mock("@ledgerhq/logs", () => ({
|
|
7
|
+
log: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock("@ledgerhq/coin-framework/crypto-assets/index", () => ({
|
|
11
|
+
getCryptoAssetsStore: () => ({
|
|
12
|
+
findTokenByAddress: jest.fn(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("erc20/tokenAccounts", () => {
|
|
17
|
+
describe("erc20TxnToOperation", () => {
|
|
18
|
+
it("should convert ERC20 send transaction to OUT operation", () => {
|
|
19
|
+
const tx = createMockERC20Transfer({
|
|
20
|
+
from: TEST_ADDRESSES.F4_ADDRESS,
|
|
21
|
+
to: TEST_ADDRESSES.RECIPIENT_F4,
|
|
22
|
+
amount: "1000000000000000000",
|
|
23
|
+
status: TxStatus.Ok,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const accountId = "accountId123";
|
|
27
|
+
const ops = erc20TxnToOperation(tx, TEST_ADDRESSES.F4_ADDRESS, accountId);
|
|
28
|
+
|
|
29
|
+
expect(ops).toHaveLength(1);
|
|
30
|
+
expect(ops[0].type).toBe("OUT");
|
|
31
|
+
expect(ops[0].value.isEqualTo(new BigNumber("1000000000000000000"))).toBe(true);
|
|
32
|
+
expect(ops[0].accountId).toBe(accountId);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should convert ERC20 receive transaction to IN operation", () => {
|
|
36
|
+
const tx = createMockERC20Transfer({
|
|
37
|
+
from: TEST_ADDRESSES.F4_ADDRESS,
|
|
38
|
+
to: TEST_ADDRESSES.RECIPIENT_F4,
|
|
39
|
+
amount: "500000000000000000",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const accountId = "accountId123";
|
|
43
|
+
const ops = erc20TxnToOperation(tx, TEST_ADDRESSES.RECIPIENT_F4, accountId);
|
|
44
|
+
|
|
45
|
+
expect(ops).toHaveLength(1);
|
|
46
|
+
expect(ops[0].type).toBe("IN");
|
|
47
|
+
expect(ops[0].value.isEqualTo(new BigNumber("500000000000000000"))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle failed transaction", () => {
|
|
51
|
+
const tx = createMockERC20Transfer({
|
|
52
|
+
status: "Fail",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ops = erc20TxnToOperation(tx, TEST_ADDRESSES.F4_ADDRESS, "accountId");
|
|
56
|
+
|
|
57
|
+
expect(ops[0].hasFailed).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle self-transfer (both send and receive)", () => {
|
|
61
|
+
const tx = createMockERC20Transfer({
|
|
62
|
+
from: TEST_ADDRESSES.F4_ADDRESS,
|
|
63
|
+
to: TEST_ADDRESSES.F4_ADDRESS,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const ops = erc20TxnToOperation(tx, TEST_ADDRESSES.F4_ADDRESS, "accountId");
|
|
67
|
+
|
|
68
|
+
expect(ops).toHaveLength(2);
|
|
69
|
+
expect(ops.some(op => op.type === "OUT")).toBe(true);
|
|
70
|
+
expect(ops.some(op => op.type === "IN")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|