@ledgerhq/coin-filecoin 1.14.1-nightly.20251114023758 → 1.15.0-nightly.20251118023800

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 (56) hide show
  1. package/.unimportedrc.json +2 -0
  2. package/CHANGELOG.md +11 -7
  3. package/lib/api/api.d.ts +5 -3
  4. package/lib/api/api.d.ts.map +1 -1
  5. package/lib/api/api.js +66 -21
  6. package/lib/api/api.js.map +1 -1
  7. package/lib/common-logic/index.d.ts +1 -1
  8. package/lib/common-logic/index.d.ts.map +1 -1
  9. package/lib/common-logic/index.js +1 -3
  10. package/lib/common-logic/index.js.map +1 -1
  11. package/lib/common-logic/utils.d.ts +0 -2
  12. package/lib/common-logic/utils.d.ts.map +1 -1
  13. package/lib/common-logic/utils.js +16 -68
  14. package/lib/common-logic/utils.js.map +1 -1
  15. package/lib/erc20/tokenAccounts.d.ts +2 -3
  16. package/lib/erc20/tokenAccounts.d.ts.map +1 -1
  17. package/lib/erc20/tokenAccounts.js +46 -32
  18. package/lib/erc20/tokenAccounts.js.map +1 -1
  19. package/lib/test/fixtures.d.ts +87 -0
  20. package/lib/test/fixtures.d.ts.map +1 -0
  21. package/lib/test/fixtures.js +311 -0
  22. package/lib/test/fixtures.js.map +1 -0
  23. package/lib/types/common.d.ts +22 -2
  24. package/lib/types/common.d.ts.map +1 -1
  25. package/lib-es/api/api.d.ts +5 -3
  26. package/lib-es/api/api.d.ts.map +1 -1
  27. package/lib-es/api/api.js +61 -18
  28. package/lib-es/api/api.js.map +1 -1
  29. package/lib-es/common-logic/index.d.ts +1 -1
  30. package/lib-es/common-logic/index.d.ts.map +1 -1
  31. package/lib-es/common-logic/index.js +1 -1
  32. package/lib-es/common-logic/index.js.map +1 -1
  33. package/lib-es/common-logic/utils.d.ts +0 -2
  34. package/lib-es/common-logic/utils.d.ts.map +1 -1
  35. package/lib-es/common-logic/utils.js +16 -66
  36. package/lib-es/common-logic/utils.js.map +1 -1
  37. package/lib-es/erc20/tokenAccounts.d.ts +2 -3
  38. package/lib-es/erc20/tokenAccounts.d.ts.map +1 -1
  39. package/lib-es/erc20/tokenAccounts.js +47 -33
  40. package/lib-es/erc20/tokenAccounts.js.map +1 -1
  41. package/lib-es/test/fixtures.d.ts +87 -0
  42. package/lib-es/test/fixtures.d.ts.map +1 -0
  43. package/lib-es/test/fixtures.js +297 -0
  44. package/lib-es/test/fixtures.js.map +1 -0
  45. package/lib-es/types/common.d.ts +22 -2
  46. package/lib-es/types/common.d.ts.map +1 -1
  47. package/package.json +6 -6
  48. package/src/api/api.ts +107 -26
  49. package/src/api/api.unit.test.ts +217 -0
  50. package/src/common-logic/index.ts +0 -2
  51. package/src/common-logic/utils.ts +19 -90
  52. package/src/common-logic/utils.unit.test.ts +429 -0
  53. package/src/erc20/tokenAccounts.ts +59 -34
  54. package/src/erc20/tokenAccounts.unit.test.ts +73 -0
  55. package/src/test/fixtures.ts +342 -0
  56. package/src/types/common.ts +24 -2
@@ -0,0 +1,217 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
2
+ import network from "@ledgerhq/live-network";
3
+ import { getEnv } from "@ledgerhq/live-env";
4
+ import {
5
+ fetchBalances,
6
+ fetchEstimatedFees,
7
+ fetchBlockHeight,
8
+ fetchTxs,
9
+ fetchTxsWithPages,
10
+ broadcastTx,
11
+ fetchERC20TokenBalance,
12
+ fetchERC20TransactionsWithPages,
13
+ } from "./api";
14
+ import {
15
+ createMockBalanceResponse,
16
+ createMockEstimatedFeesResponse,
17
+ createMockTransactionResponse,
18
+ createMockERC20Transfer,
19
+ TEST_ADDRESSES,
20
+ TEST_TRANSACTION_HASHES,
21
+ TEST_BLOCK_HEIGHTS,
22
+ } from "../test/fixtures";
23
+
24
+ // Mock dependencies
25
+ jest.mock("@ledgerhq/logs");
26
+ jest.mock("@ledgerhq/live-network/network");
27
+ jest.mock("@ledgerhq/live-env");
28
+
29
+ const MOCK_API_URL = "https://mock.filecoin.api";
30
+ const mockedNetwork = network as jest.MockedFunction<typeof network>;
31
+ const mockedGetEnv = getEnv as jest.MockedFunction<typeof getEnv>;
32
+
33
+ describe("Filecoin API", () => {
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ jest.resetAllMocks();
37
+ mockedGetEnv.mockReturnValue(MOCK_API_URL);
38
+ });
39
+
40
+ describe("fetchBalances", () => {
41
+ it("should fetch balance for a given address", async () => {
42
+ const mockBalance = createMockBalanceResponse({
43
+ total_balance: "1000000000000000000",
44
+ spendable_balance: "900000000000000000",
45
+ locked_balance: "100000000000000000",
46
+ });
47
+
48
+ mockedNetwork.mockResolvedValueOnce({ data: mockBalance, status: 200 });
49
+
50
+ const result = await fetchBalances(TEST_ADDRESSES.F1_ADDRESS);
51
+
52
+ expect(result).toEqual(mockBalance);
53
+ });
54
+ });
55
+
56
+ describe("fetchEstimatedFees", () => {
57
+ it("should fetch estimated fees for a transaction", async () => {
58
+ const mockFees = createMockEstimatedFeesResponse({
59
+ gas_limit: 1500000,
60
+ gas_fee_cap: "150000",
61
+ gas_premium: "125000",
62
+ nonce: 5,
63
+ });
64
+
65
+ const request = {
66
+ from: TEST_ADDRESSES.F1_ADDRESS,
67
+ to: TEST_ADDRESSES.RECIPIENT_F1,
68
+ };
69
+
70
+ mockedNetwork.mockResolvedValueOnce({ data: mockFees, status: 200 });
71
+
72
+ const result = await fetchEstimatedFees(request);
73
+
74
+ expect(result).toEqual(mockFees);
75
+ });
76
+ });
77
+
78
+ describe("fetchBlockHeight", () => {
79
+ it("should fetch current block height", async () => {
80
+ const mockNetworkStatus = {
81
+ current_block_identifier: {
82
+ index: TEST_BLOCK_HEIGHTS.CURRENT,
83
+ hash: "blockhash123",
84
+ },
85
+ genesis_block_identifier: {
86
+ index: 0,
87
+ hash: "genesis",
88
+ },
89
+ current_block_timestamp: Date.now(),
90
+ };
91
+
92
+ mockedNetwork.mockResolvedValueOnce({ data: mockNetworkStatus, status: 200 });
93
+
94
+ const result = await fetchBlockHeight();
95
+
96
+ expect(result.current_block_identifier.index).toBe(TEST_BLOCK_HEIGHTS.CURRENT);
97
+ });
98
+ });
99
+
100
+ describe("fetchTxs", () => {
101
+ it("should fetch transactions from specific height", async () => {
102
+ const mockResponse = {
103
+ txs: [createMockTransactionResponse()],
104
+ metadata: { limit: 50, offset: 10 },
105
+ };
106
+
107
+ mockedNetwork.mockResolvedValueOnce({ data: mockResponse, status: 200 });
108
+
109
+ await fetchTxs(TEST_ADDRESSES.F1_ADDRESS, 2500000, 10, 50);
110
+
111
+ expect(mockedNetwork).toHaveBeenCalledWith({
112
+ method: "GET",
113
+ url: expect.stringContaining("from_height=2500000&offset=10&limit=50"),
114
+ });
115
+ });
116
+ });
117
+
118
+ describe("fetchTxsWithPages", () => {
119
+ it("should fetch all transactions with multi-page pagination", async () => {
120
+ const firstPageTxs = Array.from({ length: 1000 }, (_, i) =>
121
+ createMockTransactionResponse({ hash: `hash_${i}` }),
122
+ );
123
+ const secondPageTxs = Array.from({ length: 500 }, (_, i) =>
124
+ createMockTransactionResponse({ hash: `hash_${1000 + i}` }),
125
+ );
126
+
127
+ mockedNetwork
128
+ .mockResolvedValueOnce({ data: { txs: firstPageTxs, metadata: {} }, status: 200 })
129
+ .mockResolvedValueOnce({ data: { txs: secondPageTxs, metadata: {} }, status: 200 });
130
+
131
+ const result = await fetchTxsWithPages(TEST_ADDRESSES.F1_ADDRESS, 0);
132
+
133
+ expect(result).toHaveLength(1500);
134
+ expect(mockedNetwork).toHaveBeenCalledTimes(2);
135
+ });
136
+ });
137
+
138
+ describe("broadcastTx", () => {
139
+ it("should broadcast a signed transaction", async () => {
140
+ const mockRequest = {
141
+ message: {
142
+ version: 0,
143
+ to: TEST_ADDRESSES.RECIPIENT_F1,
144
+ from: TEST_ADDRESSES.F1_ADDRESS,
145
+ nonce: 5,
146
+ value: "100000000000000000",
147
+ gaslimit: 1000000,
148
+ gasfeecap: "100000",
149
+ gaspremium: "100000",
150
+ method: 0,
151
+ params: "",
152
+ },
153
+ signature: {
154
+ type: 1,
155
+ data: "signature_data_here",
156
+ },
157
+ };
158
+
159
+ const mockResponse = {
160
+ hash: TEST_TRANSACTION_HASHES.VALID,
161
+ };
162
+
163
+ mockedNetwork.mockResolvedValueOnce({ data: mockResponse, status: 200 });
164
+
165
+ const result = await broadcastTx(mockRequest);
166
+
167
+ expect(result).toBeDefined();
168
+ });
169
+ });
170
+
171
+ describe("fetchERC20TokenBalance", () => {
172
+ it("should return 0 when no balance data is available", async () => {
173
+ const mockResponse = {
174
+ data: [],
175
+ };
176
+
177
+ mockedNetwork.mockResolvedValueOnce({ data: mockResponse, status: 200 });
178
+
179
+ const result = await fetchERC20TokenBalance(
180
+ TEST_ADDRESSES.F4_ADDRESS,
181
+ TEST_ADDRESSES.ERC20_CONTRACT,
182
+ );
183
+
184
+ expect(result).toBe("0");
185
+ });
186
+ });
187
+
188
+ describe("fetchERC20TransactionsWithPages", () => {
189
+ it("should fetch all ERC20 transactions with pagination and sort by timestamp", async () => {
190
+ const now = Math.floor(Date.now() / 1000);
191
+
192
+ const firstPageTxs = Array.from({ length: 1000 }, (_, i) =>
193
+ createMockERC20Transfer({
194
+ id: `${i}`,
195
+ timestamp: now - i,
196
+ }),
197
+ );
198
+
199
+ const secondPageTxs = Array.from({ length: 300 }, (_, i) =>
200
+ createMockERC20Transfer({
201
+ id: `${1000 + i}`,
202
+ timestamp: now - 1000 - i,
203
+ }),
204
+ );
205
+
206
+ mockedNetwork
207
+ .mockResolvedValueOnce({ data: { txs: firstPageTxs }, status: 200 })
208
+ .mockResolvedValueOnce({ data: { txs: secondPageTxs }, status: 200 });
209
+
210
+ const result = await fetchERC20TransactionsWithPages(TEST_ADDRESSES.F4_ADDRESS, 100);
211
+
212
+ expect(result).toHaveLength(1300);
213
+ expect(mockedNetwork).toHaveBeenCalledTimes(2);
214
+ expect(result[0].timestamp).toBeGreaterThanOrEqual(result[1].timestamp);
215
+ });
216
+ });
217
+ });
@@ -1,6 +1,4 @@
1
1
  export {
2
- getUnit,
3
- processTxs,
4
2
  mapTxToOps,
5
3
  getAddress,
6
4
  getTxToBroadcast,
@@ -1,98 +1,23 @@
1
1
  import { Account, Operation } from "@ledgerhq/types-live";
2
2
  import type { Unit } from "@ledgerhq/types-cryptoassets";
3
- import { log } from "@ledgerhq/logs";
4
- import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies";
5
- import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
6
3
  import { BigNumber } from "bignumber.js";
7
4
  import { BroadcastTransactionRequest, TransactionResponse, TxStatus, Transaction } from "../types";
8
5
  import { GetAccountShape, AccountShapeInfo } from "@ledgerhq/coin-framework/bridge/jsHelpers";
9
- import { fetchBalances, fetchBlockHeight, fetchTxs } from "../api/api";
6
+ import { fetchBalances, fetchBlockHeight, fetchTxsWithPages } from "../api/api";
10
7
  import { encodeAccountId } from "@ledgerhq/coin-framework/account";
11
8
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
12
9
  import flatMap from "lodash/flatMap";
13
10
  import { buildTokenAccounts } from "../erc20/tokenAccounts";
14
11
 
15
- type TxsById = {
16
- [id: string]:
17
- | {
18
- Send: TransactionResponse;
19
- Fee?: TransactionResponse;
20
- }
21
- | {
22
- InvokeContract: TransactionResponse;
23
- Fee?: TransactionResponse;
24
- };
25
- };
26
-
27
- export const getUnit = () => getCryptoCurrencyById("filecoin").units[0];
28
-
29
- export const processTxs = (txs: TransactionResponse[]): TransactionResponse[] => {
30
- // Group all tx types related to same tx cid into the same object
31
- const txsByTxCid = txs.reduce((txsByTxCidResult: TxsById, currentTx) => {
32
- const { hash: txCid, type: txType } = currentTx;
33
- const txByType = txsByTxCidResult[txCid] || {};
34
- switch (txType) {
35
- case "Send":
36
- (txByType as { Send: TransactionResponse }).Send = currentTx;
37
- break;
38
- case "InvokeContract":
39
- (txByType as { InvokeContract: TransactionResponse }).InvokeContract = currentTx;
40
- break;
41
- case "Fee":
42
- (txByType as { Fee?: TransactionResponse }).Fee = currentTx;
43
- break;
44
- default:
45
- log("warn", `tx type [${txType}] on tx cid [${txCid}] was not recognized.`);
46
- break;
47
- }
48
-
49
- txsByTxCidResult[txCid] = txByType;
50
- return txsByTxCidResult;
51
- }, {});
52
-
53
- // Once all tx types have been grouped, we want to find
54
- const processedTxs: TransactionResponse[] = [];
55
- for (const txCid in txsByTxCid) {
56
- const item = txsByTxCid[txCid];
57
- const feeTx = item.Fee;
58
- let mainTx: TransactionResponse | undefined;
59
- if ("Send" in item) {
60
- mainTx = item.Send;
61
- } else if ("InvokeContract" in item) {
62
- mainTx = item.InvokeContract;
63
- } else {
64
- log(
65
- "warn",
66
- `unexpected tx type, tx with cid [${txCid}] and payload [${JSON.stringify(item)}]`,
67
- );
68
- }
69
-
70
- if (!mainTx) {
71
- if (feeTx) {
72
- log("warn", `feeTx [${feeTx.hash}] found without a mainTx linked to it.`);
73
- }
74
-
75
- continue;
76
- }
77
-
78
- if (feeTx) {
79
- mainTx.fee = feeTx.amount;
80
- }
81
-
82
- processedTxs.push(mainTx);
83
- }
84
-
85
- return processedTxs;
86
- };
87
-
88
12
  export const mapTxToOps =
89
13
  (accountId: string, { address }: AccountShapeInfo) =>
90
14
  (tx: TransactionResponse): Operation[] => {
91
- const { to, from, hash, timestamp, amount, fee, status } = tx;
15
+ const { to, from, hash, timestamp, amount, fee_data, status } = tx;
16
+
92
17
  const ops: Operation[] = [];
93
18
  const date = new Date(timestamp * 1000);
94
- const value = parseCurrencyUnit(getUnit(), amount.toString());
95
- const feeToUse = parseCurrencyUnit(getUnit(), (fee || 0).toString());
19
+ const value = new BigNumber(amount);
20
+ const feeToUse = new BigNumber(fee_data?.TotalCost || 0);
96
21
 
97
22
  const isSending = address === from;
98
23
  const isReceiving = address === to;
@@ -186,7 +111,11 @@ export const getTxToBroadcast = (
186
111
  };
187
112
 
188
113
  export const getAccountShape: GetAccountShape = async info => {
189
- const { address, currency, derivationMode } = info;
114
+ const { address, currency, derivationMode, initialAccount } = info;
115
+
116
+ const blockSafeDelta = 1200;
117
+ let lastHeight = (initialAccount?.blockHeight ?? 0) - blockSafeDelta;
118
+ if (lastHeight < 0) lastHeight = 0;
190
119
 
191
120
  const accountId = encodeAccountId({
192
121
  type: "js",
@@ -196,21 +125,21 @@ export const getAccountShape: GetAccountShape = async info => {
196
125
  derivationMode,
197
126
  });
198
127
 
199
- const blockHeight = await fetchBlockHeight();
200
- const balance = await fetchBalances(address);
201
- const rawTxs = await fetchTxs(address);
202
- const tokenAccounts = await buildTokenAccounts(address, accountId, info.initialAccount);
203
- const operations = flatMap(processTxs(rawTxs), mapTxToOps(accountId, info)).sort(
204
- (a, b) => b.date.getTime() - a.date.getTime(),
205
- );
128
+ const [blockHeight, balance, rawTxs, tokenAccounts] = await Promise.all([
129
+ fetchBlockHeight(),
130
+ fetchBalances(address),
131
+ fetchTxsWithPages(address, lastHeight),
132
+ buildTokenAccounts(address, lastHeight, accountId, info.initialAccount),
133
+ ]);
206
134
 
207
135
  const result: Partial<Account> = {
208
136
  id: accountId,
209
137
  subAccounts: tokenAccounts,
210
138
  balance: new BigNumber(balance.total_balance),
211
139
  spendableBalance: new BigNumber(balance.spendable_balance),
212
- operations,
213
- operationsCount: operations.length,
140
+ operations: flatMap(rawTxs, mapTxToOps(accountId, info)).sort(
141
+ (a, b) => b.date.getTime() - a.date.getTime(),
142
+ ),
214
143
  blockHeight: blockHeight.current_block_identifier.index,
215
144
  };
216
145
  return result;