@ledgerhq/coin-evm 0.2.0-next.0

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 (53) hide show
  1. package/.eslintrc.js +57 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/CHANGELOG.md +18 -0
  4. package/jest.config.js +6 -0
  5. package/package.json +102 -0
  6. package/src/__tests__/adapters.unit.test.ts +527 -0
  7. package/src/__tests__/broadcast.unit.test.ts +181 -0
  8. package/src/__tests__/buildOptimisticOperation.unit.test.ts +182 -0
  9. package/src/__tests__/createTransaction.unit.test.ts +52 -0
  10. package/src/__tests__/deviceTransactionConfig.unit.test.ts +245 -0
  11. package/src/__tests__/estimateMaxSpendable.unit.test.ts +123 -0
  12. package/src/__tests__/getTransactionStatus.unit.test.ts +355 -0
  13. package/src/__tests__/hw-getAddress.unit.test.ts +24 -0
  14. package/src/__tests__/logic.unit.test.ts +406 -0
  15. package/src/__tests__/preload.unit.test.ts +139 -0
  16. package/src/__tests__/prepareTransaction.unit.test.ts +394 -0
  17. package/src/__tests__/rpc.unit.test.ts +532 -0
  18. package/src/__tests__/signOperation.unit.test.ts +157 -0
  19. package/src/__tests__/synchronization.unit.test.ts +832 -0
  20. package/src/__tests__/transaction.unit.test.ts +196 -0
  21. package/src/abis/erc20.abi.json +230 -0
  22. package/src/abis/optimismGasPriceOracle.abi.json +252 -0
  23. package/src/adapters.ts +148 -0
  24. package/src/api/etherscan.ts +124 -0
  25. package/src/api/rpc.common.ts +354 -0
  26. package/src/api/rpc.native.ts +5 -0
  27. package/src/api/rpc.ts +2 -0
  28. package/src/bridge/js.ts +77 -0
  29. package/src/bridge.integration.test.ts +93 -0
  30. package/src/broadcast.ts +40 -0
  31. package/src/buildOptimisticOperation.ts +113 -0
  32. package/src/cli-transaction.ts +11 -0
  33. package/src/createTransaction.ts +25 -0
  34. package/src/datasets/ethereum.scanAccounts.1.ts +48 -0
  35. package/src/datasets/ethereum1.ts +20 -0
  36. package/src/datasets/ethereum2.ts +20 -0
  37. package/src/datasets/ethereum_classic.ts +68 -0
  38. package/src/deviceTransactionConfig.ts +64 -0
  39. package/src/errors.ts +5 -0
  40. package/src/estimateMaxSpendable.ts +19 -0
  41. package/src/getTransactionStatus.ts +186 -0
  42. package/src/hw-getAddress.ts +24 -0
  43. package/src/logic.ts +149 -0
  44. package/src/preload.ts +54 -0
  45. package/src/prepareTransaction.ts +176 -0
  46. package/src/signOperation.ts +127 -0
  47. package/src/specs.ts +344 -0
  48. package/src/speculos-deviceActions.ts +83 -0
  49. package/src/synchronization.ts +317 -0
  50. package/src/testUtils.ts +153 -0
  51. package/src/transaction.ts +193 -0
  52. package/src/types.ts +132 -0
  53. package/tsconfig.json +12 -0
@@ -0,0 +1,123 @@
1
+ import { getCryptoCurrencyById, getTokenById } from "@ledgerhq/cryptoassets";
2
+ import BigNumber from "bignumber.js";
3
+ import * as rpcAPI from "../api/rpc.common";
4
+ import { estimateMaxSpendable } from "../estimateMaxSpendable";
5
+ import { makeAccount, makeTokenAccount } from "../testUtils";
6
+ import { EvmTransactionEIP1559, EvmTransactionLegacy } from "../types";
7
+
8
+ const tokenAccount = {
9
+ ...makeTokenAccount("0xkvn", getTokenById("ethereum/erc20/usd__coin")),
10
+ balance: new BigNumber(6969),
11
+ };
12
+ const account = {
13
+ ...makeAccount("0xkvn", getCryptoCurrencyById("ethereum"), [tokenAccount]),
14
+ balance: new BigNumber(42069000000),
15
+ };
16
+
17
+ jest
18
+ .spyOn(rpcAPI, "getGasEstimation")
19
+ .mockImplementation(async () => new BigNumber(21000));
20
+ jest.spyOn(rpcAPI, "getFeesEstimation").mockImplementation(async () => ({
21
+ gasPrice: new BigNumber(10000),
22
+ maxFeePerGas: new BigNumber(10000),
23
+ maxPriorityFeePerGas: new BigNumber(0),
24
+ }));
25
+
26
+ describe("EVM Family", () => {
27
+ describe("estimateMaxSpendable.ts", () => {
28
+ it("should get a max spendable of the account balance minus the EIP1559 coin tx", async () => {
29
+ const eip1559Tx: EvmTransactionEIP1559 = {
30
+ amount: new BigNumber(100),
31
+ useAllAmount: false,
32
+ recipient: "0xlmb",
33
+ family: "evm",
34
+ mode: "send",
35
+ nonce: 0,
36
+ gasLimit: new BigNumber(0),
37
+ chainId: 1,
38
+ maxFeePerGas: new BigNumber(0),
39
+ maxPriorityFeePerGas: new BigNumber(0),
40
+ type: 2,
41
+ };
42
+ const amount = await estimateMaxSpendable({
43
+ account,
44
+ transaction: eip1559Tx,
45
+ });
46
+ const gasLimit = await rpcAPI.getGasEstimation(account, eip1559Tx);
47
+ const { maxFeePerGas } = await rpcAPI.getFeesEstimation(account.currency);
48
+ const estimatedFees = gasLimit.times(maxFeePerGas!);
49
+ expect(amount).toEqual(account.balance.minus(estimatedFees));
50
+ });
51
+
52
+ it("should get a max spendable of the account balance minus the legacy coin tx", async () => {
53
+ const legacyTx: EvmTransactionLegacy = {
54
+ amount: new BigNumber(100),
55
+ useAllAmount: false,
56
+ recipient: "0xlmb",
57
+ family: "evm",
58
+ mode: "send",
59
+ nonce: 0,
60
+ gasLimit: new BigNumber(0),
61
+ chainId: 1,
62
+ gasPrice: new BigNumber(0),
63
+ type: 0,
64
+ };
65
+ const amount = await estimateMaxSpendable({
66
+ account,
67
+ transaction: legacyTx,
68
+ });
69
+ const gasLimit = await rpcAPI.getGasEstimation(account, legacyTx);
70
+ const { gasPrice } = await rpcAPI.getFeesEstimation(account.currency);
71
+ const estimatedFees = gasLimit.times(gasPrice!);
72
+ expect(amount).toEqual(account.balance.minus(estimatedFees));
73
+ });
74
+
75
+ it("should get a max spendable of the account balance minus the EIP1559 token tx", async () => {
76
+ const eip1559Tx: EvmTransactionEIP1559 = {
77
+ amount: new BigNumber(100),
78
+ useAllAmount: false,
79
+ recipient: "0xlmb",
80
+ family: "evm",
81
+ mode: "send",
82
+ nonce: 0,
83
+ gasLimit: new BigNumber(0),
84
+ chainId: 1,
85
+ maxFeePerGas: new BigNumber(0),
86
+ maxPriorityFeePerGas: new BigNumber(0),
87
+ subAccountId: tokenAccount.id,
88
+ type: 2,
89
+ };
90
+
91
+ const amount = await estimateMaxSpendable({
92
+ account: tokenAccount,
93
+ parentAccount: account,
94
+ transaction: eip1559Tx,
95
+ });
96
+
97
+ expect(amount).toEqual(tokenAccount.balance);
98
+ });
99
+
100
+ it("should get a max spendable of the account balance minus the legacy token tx", async () => {
101
+ const legacyTx: EvmTransactionLegacy = {
102
+ amount: new BigNumber(100),
103
+ useAllAmount: false,
104
+ recipient: "0xlmb",
105
+ family: "evm",
106
+ mode: "send",
107
+ nonce: 0,
108
+ gasLimit: new BigNumber(0),
109
+ chainId: 1,
110
+ gasPrice: new BigNumber(0),
111
+ subAccountId: tokenAccount.id,
112
+ type: 0,
113
+ };
114
+ const amount = await estimateMaxSpendable({
115
+ account: tokenAccount,
116
+ parentAccount: account,
117
+ transaction: legacyTx,
118
+ });
119
+
120
+ expect(amount).toEqual(tokenAccount.balance);
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,355 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { getCryptoCurrencyById, getTokenById } from "@ledgerhq/cryptoassets";
3
+ import {
4
+ AmountRequired,
5
+ ETHAddressNonEIP,
6
+ FeeNotLoaded,
7
+ GasLessThanEstimate,
8
+ InvalidAddress,
9
+ NotEnoughBalance,
10
+ NotEnoughGas,
11
+ PriorityFeeTooLow,
12
+ RecipientRequired,
13
+ } from "@ledgerhq/errors";
14
+ import BigNumber from "bignumber.js";
15
+ import getTransactionStatus from "../getTransactionStatus";
16
+ import { makeAccount, makeTokenAccount } from "../testUtils";
17
+ import { EvmTransactionEIP1559, EvmTransactionLegacy } from "../types";
18
+
19
+ const recipient = "0xe2ca7390e76c5A992749bB622087310d2e63ca29"; // rambo.eth
20
+ const testData = Buffer.from("testBufferString").toString("hex");
21
+ const tokenAccount = makeTokenAccount(
22
+ "0xkvn",
23
+ getTokenById("ethereum/erc20/usd__coin")
24
+ );
25
+ const account = makeAccount("0xkvn", getCryptoCurrencyById("ethereum"), [
26
+ tokenAccount,
27
+ ]);
28
+ const legacyTx: EvmTransactionLegacy = {
29
+ amount: new BigNumber(100),
30
+ useAllAmount: false,
31
+ subAccountId: "id",
32
+ recipient,
33
+ feesStrategy: "custom",
34
+ family: "evm",
35
+ mode: "send",
36
+ nonce: 0,
37
+ gasLimit: new BigNumber(21000),
38
+ chainId: 1,
39
+ data: Buffer.from(testData, "hex"),
40
+ gasPrice: new BigNumber(100),
41
+ type: 0,
42
+ };
43
+ const eip1559Tx: EvmTransactionEIP1559 = {
44
+ amount: new BigNumber(100),
45
+ useAllAmount: false,
46
+ subAccountId: "id",
47
+ recipient,
48
+ feesStrategy: "custom",
49
+ family: "evm",
50
+ mode: "send",
51
+ nonce: 0,
52
+ gasLimit: new BigNumber(21000),
53
+ chainId: 1,
54
+ data: Buffer.from(testData, "hex"),
55
+ maxFeePerGas: new BigNumber(100),
56
+ maxPriorityFeePerGas: new BigNumber(100),
57
+ type: 2,
58
+ };
59
+
60
+ describe("EVM Family", () => {
61
+ describe("getTransactionStatus.ts", () => {
62
+ describe("Recipient", () => {
63
+ it("should detect the missing recipient and have an error", async () => {
64
+ const tx = { ...eip1559Tx, recipient: "" };
65
+ const res = await getTransactionStatus(account, tx);
66
+ expect(res.errors).toEqual(
67
+ expect.objectContaining({
68
+ recipient: new RecipientRequired(),
69
+ })
70
+ );
71
+ });
72
+
73
+ it("should detect the incorrect recipient not being an eth address and have an error", async () => {
74
+ const tx = { ...eip1559Tx, recipient: "0xkvn" };
75
+ const res = await getTransactionStatus(account, tx);
76
+ expect(res.errors).toEqual(
77
+ expect.objectContaining({
78
+ recipient: new InvalidAddress("", {
79
+ currency: account.currency,
80
+ }),
81
+ })
82
+ );
83
+ });
84
+
85
+ it("should detect the recipient being an ICAP and have an error", async () => {
86
+ const tx = {
87
+ ...eip1559Tx,
88
+ recipient: "XE89MW3Y75UITCQ4F53YDKR25UFLB1640YM", // ICAP version of recipient address
89
+ };
90
+ const res = await getTransactionStatus(account, tx);
91
+ expect(res.errors).toEqual(
92
+ expect.objectContaining({
93
+ recipient: new InvalidAddress("", {
94
+ currency: account.currency,
95
+ }),
96
+ })
97
+ );
98
+ });
99
+
100
+ it("should detect the recipient not being an EIP55 address and have an warning", async () => {
101
+ const tx = { ...eip1559Tx, recipient: recipient.toLowerCase() };
102
+ const res = await getTransactionStatus(account, tx);
103
+ expect(res.warnings).toEqual(
104
+ expect.objectContaining({
105
+ recipient: new ETHAddressNonEIP(),
106
+ })
107
+ );
108
+ });
109
+ });
110
+
111
+ describe("Amount", () => {
112
+ it("should detect tx without amount and have an error", async () => {
113
+ const tx = {
114
+ ...eip1559Tx,
115
+ amount: new BigNumber(0),
116
+ data: undefined,
117
+ };
118
+ const res = await getTransactionStatus(account, tx);
119
+
120
+ expect(res.errors).toEqual(
121
+ expect.objectContaining({
122
+ amount: new AmountRequired(),
123
+ })
124
+ );
125
+ });
126
+
127
+ it("should detect tx without amount but with data and not return error", async () => {
128
+ const tx = {
129
+ ...eip1559Tx,
130
+ amount: new BigNumber(0),
131
+ };
132
+ const res = await getTransactionStatus(
133
+ { ...account, balance: new BigNumber(10000000) },
134
+ tx
135
+ );
136
+
137
+ expect(res.errors).toEqual({});
138
+ });
139
+
140
+ it("should detect tx without amount (because of useAllAmount) but from tokenAccount and not return error", async () => {
141
+ const tx = {
142
+ ...eip1559Tx,
143
+ amount: new BigNumber(0),
144
+ useAllAmount: true,
145
+ subAccountId: tokenAccount.id,
146
+ };
147
+ const res = await getTransactionStatus(
148
+ {
149
+ ...account,
150
+ balance: new BigNumber(10000000),
151
+ subAccounts: [
152
+ {
153
+ ...tokenAccount,
154
+ balance: new BigNumber(10),
155
+ },
156
+ ],
157
+ },
158
+ tx
159
+ );
160
+
161
+ expect(res.errors).toEqual({});
162
+ });
163
+
164
+ it("should detect account not having enough balance for a tx and have an error", async () => {
165
+ const res = await getTransactionStatus(
166
+ { ...account, balance: new BigNumber(0) },
167
+ eip1559Tx
168
+ );
169
+
170
+ expect(res.errors).toEqual(
171
+ expect.objectContaining({
172
+ amount: new NotEnoughBalance(),
173
+ })
174
+ );
175
+ });
176
+ });
177
+
178
+ describe("Gas", () => {
179
+ it("should detect missing fees in a 1559 tx and have an error", async () => {
180
+ const tx = { ...eip1559Tx, maxFeePerGas: undefined };
181
+ const res = await getTransactionStatus(account, tx as any);
182
+
183
+ expect(res.errors).toEqual(
184
+ expect.objectContaining({
185
+ gasPrice: new FeeNotLoaded(),
186
+ })
187
+ );
188
+ });
189
+
190
+ it("should detect missing fees in a legacy tx and have an error", async () => {
191
+ const tx = { ...legacyTx, gasPrice: undefined };
192
+ const res = await getTransactionStatus(account, tx as any);
193
+
194
+ expect(res.errors).toEqual(
195
+ expect.objectContaining({
196
+ gasPrice: new FeeNotLoaded(),
197
+ })
198
+ );
199
+ });
200
+
201
+ it("should detect a gasLimit = 0 in a 1559 tx and have an error", async () => {
202
+ const tx = { ...eip1559Tx, gasLimit: new BigNumber(0) };
203
+ const res = await getTransactionStatus(account, tx as any);
204
+
205
+ expect(res.errors).toEqual(
206
+ expect.objectContaining({
207
+ gasLimit: new FeeNotLoaded(),
208
+ })
209
+ );
210
+ });
211
+
212
+ it("should detect a gasLimit = 0 in a legacy tx and have an error", async () => {
213
+ const tx = { ...legacyTx, gasLimit: new BigNumber(0) };
214
+ const res = await getTransactionStatus(account, tx as any);
215
+
216
+ expect(res.errors).toEqual(
217
+ expect.objectContaining({
218
+ gasLimit: new FeeNotLoaded(),
219
+ })
220
+ );
221
+ });
222
+
223
+ it("should detect gas limit being too low in a tx and have an error", async () => {
224
+ const tx = { ...eip1559Tx, gasLimit: new BigNumber(20000) }; // min should be 21000
225
+ const res = await getTransactionStatus(account, tx);
226
+
227
+ expect(res.errors).toEqual(
228
+ expect.objectContaining({
229
+ gasLimit: new GasLessThanEstimate(),
230
+ })
231
+ );
232
+ });
233
+
234
+ it("should detect gas being too high in a 1559 tx for the account balance and have an error", async () => {
235
+ const notEnoughBalanceResponse = await getTransactionStatus(
236
+ { ...account, balance: new BigNumber(2099999) },
237
+ eip1559Tx
238
+ );
239
+ const enoughhBalanceResponse = await getTransactionStatus(
240
+ { ...account, balance: new BigNumber(2100000) },
241
+ eip1559Tx
242
+ );
243
+
244
+ expect(notEnoughBalanceResponse.errors).toEqual(
245
+ expect.objectContaining({
246
+ gasPrice: new NotEnoughGas(),
247
+ })
248
+ );
249
+ expect(enoughhBalanceResponse.errors).not.toEqual(
250
+ expect.objectContaining({
251
+ gasPrice: new NotEnoughGas(),
252
+ })
253
+ );
254
+ });
255
+
256
+ it("should detect a maxPriorityFee = 0 in a 1559 tx and have an error", async () => {
257
+ const res = await getTransactionStatus(
258
+ { ...account, balance: new BigNumber(2100000) },
259
+ {
260
+ ...eip1559Tx,
261
+ maxPriorityFeePerGas: new BigNumber(0),
262
+ }
263
+ );
264
+
265
+ expect(res.errors).toEqual(
266
+ expect.objectContaining({
267
+ maxPriorityFee: new PriorityFeeTooLow(),
268
+ })
269
+ );
270
+ });
271
+
272
+ it("should detect gas being too high in a legacy tx for the account balance and have an error", async () => {
273
+ const notEnoughBalanceResponse = await getTransactionStatus(
274
+ { ...account, balance: new BigNumber(2099999) },
275
+ legacyTx
276
+ );
277
+ const enoughhBalanceResponse = await getTransactionStatus(
278
+ { ...account, balance: new BigNumber(2100001) },
279
+ legacyTx
280
+ );
281
+
282
+ expect(notEnoughBalanceResponse.errors).toEqual(
283
+ expect.objectContaining({
284
+ gasPrice: new NotEnoughGas(),
285
+ })
286
+ );
287
+ expect(enoughhBalanceResponse.errors).not.toEqual(
288
+ expect.objectContaining({
289
+ gasPrice: new NotEnoughGas(),
290
+ })
291
+ );
292
+ });
293
+
294
+ it("should not detect gas being too high in a 1559 tx when there is no recipient and have an error", async () => {
295
+ const notEnoughBalanceResponse = await getTransactionStatus(
296
+ { ...account, balance: new BigNumber(2099999) },
297
+ { ...eip1559Tx, recipient: "" }
298
+ );
299
+ const enoughhBalanceResponse = await getTransactionStatus(
300
+ { ...account, balance: new BigNumber(2100000) },
301
+ { ...eip1559Tx, recipient: "" }
302
+ );
303
+
304
+ expect(notEnoughBalanceResponse.errors).not.toEqual(
305
+ expect.objectContaining({
306
+ gasPrice: new NotEnoughGas(),
307
+ })
308
+ );
309
+ expect(enoughhBalanceResponse.errors).not.toEqual(
310
+ expect.objectContaining({
311
+ gasPrice: new NotEnoughGas(),
312
+ })
313
+ );
314
+ });
315
+
316
+ it("should not detect gas being too high in a legacy tx when there is no recipient and have an error", async () => {
317
+ const notEnoughBalanceResponse = await getTransactionStatus(
318
+ { ...account, balance: new BigNumber(2099999) },
319
+ { ...legacyTx, recipient: "" }
320
+ );
321
+ const enoughhBalanceResponse = await getTransactionStatus(
322
+ { ...account, balance: new BigNumber(2100001) },
323
+ { ...legacyTx, recipient: "" }
324
+ );
325
+
326
+ expect(notEnoughBalanceResponse.errors).not.toEqual(
327
+ expect.objectContaining({
328
+ gasPrice: new NotEnoughGas(),
329
+ })
330
+ );
331
+ expect(enoughhBalanceResponse.errors).not.toEqual(
332
+ expect.objectContaining({
333
+ gasPrice: new NotEnoughGas(),
334
+ })
335
+ );
336
+ });
337
+ });
338
+
339
+ describe("Global return", () => {
340
+ it("should return the expected informations", async () => {
341
+ const res = await getTransactionStatus(account, legacyTx);
342
+
343
+ expect(res).toEqual(
344
+ expect.objectContaining({
345
+ errors: expect.any(Object),
346
+ warnings: expect.any(Object),
347
+ estimatedFees: new BigNumber(2100000),
348
+ amount: legacyTx.amount,
349
+ totalSpent: new BigNumber(2100000).plus(legacyTx.amount),
350
+ })
351
+ );
352
+ });
353
+ });
354
+ });
355
+ });
@@ -0,0 +1,24 @@
1
+ import eip55 from "eip55";
2
+ import getAddress from "../hw-getAddress";
3
+
4
+ const address = "0xc3f95102D5c8F2c83e49Ce3Acfb905eDfb7f37dE";
5
+ jest.mock(
6
+ "@ledgerhq/hw-app-eth",
7
+ () =>
8
+ class {
9
+ getAddress = async () => ({
10
+ publicKey: "",
11
+ address: address.toLowerCase(),
12
+ });
13
+ }
14
+ );
15
+
16
+ describe("EVM Family", () => {
17
+ describe("hw-getAddress.ts", () => {
18
+ it("should return an eip 55 encoded address", async () => {
19
+ const response = await getAddress({} as any, {} as any);
20
+ expect(response.address).toBe(address);
21
+ expect(eip55.verify(response.address)).toBe(true);
22
+ });
23
+ });
24
+ });