@ledgerhq/coin-filecoin 1.14.1-nightly.20251114023758 → 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,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,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,
|
|
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,
|
|
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 =
|
|
95
|
-
const feeToUse =
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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;
|