@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,532 @@
1
+ import { delay } from "@ledgerhq/live-promise";
2
+ import { CryptoCurrency, CryptoCurrencyId } from "@ledgerhq/types-cryptoassets";
3
+ import { AssertionError, fail } from "assert";
4
+ import BigNumber from "bignumber.js";
5
+ import { ethers } from "ethers";
6
+ import * as RPC_API from "../api/rpc.common";
7
+ import { GasEstimationError, InsufficientFunds } from "../errors";
8
+ import { makeAccount } from "../testUtils";
9
+ import { Transaction as EvmTransaction, EvmTransactionLegacy } from "../types";
10
+
11
+ const fakeCurrency: Partial<CryptoCurrency> = {
12
+ id: "my_new_chain" as CryptoCurrencyId,
13
+ ethereumLikeInfo: {
14
+ chainId: 1,
15
+ rpc: "my-rpc.com",
16
+ },
17
+ units: [{ code: "ETH", name: "ETH", magnitude: 18 }],
18
+ };
19
+ const fakeCurrencyWithoutRPC: Partial<CryptoCurrency> = {
20
+ id: "my_new_chain" as CryptoCurrencyId,
21
+ ethereumLikeInfo: {
22
+ chainId: 1,
23
+ },
24
+ units: [{ code: "ETH", name: "ETH", magnitude: 18 }],
25
+ };
26
+ const account = makeAccount(
27
+ "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d",
28
+ fakeCurrency as CryptoCurrency
29
+ );
30
+
31
+ const mockedNetwork = {
32
+ name: "mockedEthereum",
33
+ chainId: 24,
34
+ };
35
+
36
+ jest.mock("@ledgerhq/live-promise");
37
+ (delay as jest.Mock).mockImplementation(
38
+ () => new Promise((resolve) => setTimeout(resolve, 1)) // mocking the delay supposed to happen after each try
39
+ );
40
+
41
+ describe("EVM Family", () => {
42
+ beforeEach(() => {
43
+ jest.resetAllMocks();
44
+ jest
45
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "getNetwork")
46
+ .mockResolvedValue(mockedNetwork);
47
+ jest
48
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "detectNetwork")
49
+ .mockResolvedValue(mockedNetwork);
50
+ jest
51
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "getResolver")
52
+ .mockResolvedValue(null);
53
+ jest
54
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "resolveName")
55
+ .mockImplementation(async (address) => address);
56
+ jest
57
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "perform")
58
+ .mockImplementation(async (method, params) => {
59
+ switch (method) {
60
+ case "getBalance":
61
+ return ethers.BigNumber.from(420);
62
+ case "getBlockNumber":
63
+ return ethers.BigNumber.from(69);
64
+ case "getTransaction":
65
+ return {
66
+ hash: "0x435b00d28a10febbcfefbdea080134d08ef843df122d5bc9174b09de7fce6a59",
67
+ confirmations: 100,
68
+ from: "0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d",
69
+ nonce: 0,
70
+ gasLimit: ethers.BigNumber.from(123),
71
+ data: "0x",
72
+ value: ethers.BigNumber.from(456),
73
+ chainId: mockedNetwork.chainId,
74
+ };
75
+ case "call":
76
+ return "0x00000000000000000000000000000000000000000000000000000000000001A4"; // 420 as uint256 hex
77
+ case "getTransactionCount":
78
+ return ethers.BigNumber.from(5);
79
+ case "estimateGas":
80
+ return ethers.BigNumber.from(5);
81
+ case "getBlock":
82
+ return {
83
+ parentHash:
84
+ "0x474dee0136108e9412e9d84197b468bb057a8dad0f2024fc55adebc4a28fa8c5",
85
+ number: 1,
86
+ timestamp: 123,
87
+ difficulty: null,
88
+ gasLimit: ethers.BigNumber.from(1),
89
+ gasUsed: ethers.BigNumber.from(2),
90
+ extraData: "0x",
91
+ baseFeePerGas: ethers.BigNumber.from(123),
92
+ };
93
+ case "getGasPrice":
94
+ return ethers.BigNumber.from(666);
95
+ case "sendTransaction":
96
+ if (
97
+ params.signedTransaction ===
98
+ "0x02f873012d85010c388d0085077715912682520894c2907efcce4011c491bbeda8a0fa63ba7aab596c87038d7ea4c6800080c001a0bbffe7ba303ab03f697d64672c4a288ae863df8a62ffc67ba72872ce8c227f6fa01261e7c9f06af13631f03fad9b88d3c48931d353b6b41b4072fddcca5ec41628"
99
+ ) {
100
+ const err = new Error();
101
+ // @ts-expect-error adding code prop
102
+ err.code = "INSUFFICIENT_FUNDS";
103
+
104
+ throw err;
105
+ } else if (
106
+ params.signedTransaction ===
107
+ "0x02f873012d85010c388d0085077715912682520894c2907efcce4011c491bbeda8a0fa63ba7aab596c87038d7ea4c6800080c001a0bbffe7ba303ab03f697d64672c4a288ae863df8a62ffc67ba72872ce8c227f6fa01261e7c9f06af13631f03fad9b88d3c48931d353b6b41b4072fddcca5ec41625"
108
+ ) {
109
+ throw new Error("any error");
110
+ }
111
+ return "0x435b00d28a10febbcfefbdea080134d08ef843df122d5bc9174b09de7fce6a59";
112
+ default:
113
+ throw Error(`Please mock this method: ${method}`);
114
+ }
115
+ });
116
+ });
117
+
118
+ describe("api/rpc.common.ts", () => {
119
+ describe("withApi", () => {
120
+ it("should throw if the currency doesn't have an RPC node", async () => {
121
+ try {
122
+ await RPC_API.withApi(
123
+ fakeCurrencyWithoutRPC as CryptoCurrency,
124
+ (() => {}) as any
125
+ );
126
+ fail("Promise should have been rejected");
127
+ } catch (e) {
128
+ if (e instanceof AssertionError) {
129
+ throw e;
130
+ }
131
+ expect((e as Error).message).toEqual(
132
+ "Currency doesn't have an RPC node provided"
133
+ );
134
+ }
135
+ });
136
+
137
+ it("should retry on fail as many times as the DEFAULT_RETRIES_RPC_METHODS constant is set to", async () => {
138
+ let retries = RPC_API.DEFAULT_RETRIES_RPC_METHODS;
139
+ const spy = jest.fn(async () => {
140
+ if (retries) {
141
+ --retries;
142
+ throw new Error();
143
+ }
144
+ return true;
145
+ });
146
+ const response = await RPC_API.withApi(
147
+ fakeCurrency as CryptoCurrency,
148
+ spy
149
+ );
150
+
151
+ expect(response).toBe(true);
152
+ // it should fail DEFAULT_RETRIES_RPC_METHODS times and succeed on the next try, therefore the +1
153
+ expect(spy).toBeCalledTimes(RPC_API.DEFAULT_RETRIES_RPC_METHODS + 1);
154
+ });
155
+
156
+ it("should throw after too many retries", async () => {
157
+ const SpyError = class SpyError extends Error {};
158
+
159
+ let retries = RPC_API.DEFAULT_RETRIES_RPC_METHODS + 1;
160
+ const spy = jest.fn(async () => {
161
+ if (retries) {
162
+ --retries;
163
+ throw new SpyError();
164
+ }
165
+ return true;
166
+ });
167
+
168
+ try {
169
+ await RPC_API.withApi(fakeCurrency as CryptoCurrency, spy);
170
+ fail("Promise should have been rejected");
171
+ } catch (e) {
172
+ if (e instanceof AssertionError) {
173
+ throw e;
174
+ }
175
+ expect(e).toBeInstanceOf(SpyError);
176
+ }
177
+ });
178
+ });
179
+
180
+ describe("getBalanceAndBlock", () => {
181
+ it("should return the expected payload", async () => {
182
+ expect(
183
+ await RPC_API.getBalanceAndBlock(
184
+ fakeCurrency as CryptoCurrency,
185
+ "0xkvn"
186
+ )
187
+ ).toEqual({
188
+ blockHeight: 69,
189
+ balance: new BigNumber(420),
190
+ });
191
+ });
192
+ });
193
+
194
+ describe("getTransaction", () => {
195
+ it("should return the expected payload", async () => {
196
+ expect(
197
+ await RPC_API.getTransaction(
198
+ fakeCurrency as CryptoCurrency,
199
+ "0x435b00d28a10febbcfefbdea080134d08ef843df122d5bc9174b09de7fce6a59"
200
+ )
201
+ ).toEqual({
202
+ accessList: null,
203
+ blockHash: null,
204
+ blockNumber: null,
205
+ chainId: mockedNetwork.chainId,
206
+ confirmations: 0,
207
+ creates: "0x46188244A4a21a43f1f6d3bCb5b3e428572f88eC",
208
+ data: "0x",
209
+ from: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d",
210
+ gasLimit: ethers.BigNumber.from(123),
211
+ hash: "0x435b00d28a10febbcfefbdea080134d08ef843df122d5bc9174b09de7fce6a59",
212
+ nonce: 0,
213
+ to: null,
214
+ transactionIndex: null,
215
+ type: 0,
216
+ value: ethers.BigNumber.from(456),
217
+ wait: expect.any(Function),
218
+ });
219
+ });
220
+ });
221
+
222
+ describe("getCoinBalance", () => {
223
+ it("should return the expected payload", async () => {
224
+ expect(
225
+ await RPC_API.getCoinBalance(
226
+ fakeCurrency as CryptoCurrency,
227
+ "0x435b00d28a10febbcfefbdea080134d08ef843df122d5bc9174b09de7fce6a59"
228
+ )
229
+ ).toEqual(new BigNumber(420));
230
+ });
231
+ });
232
+ });
233
+
234
+ describe("getTokenBalance", () => {
235
+ it("should return the expected payload", async () => {
236
+ expect(
237
+ await RPC_API.getTokenBalance(
238
+ fakeCurrency as CryptoCurrency,
239
+ "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d",
240
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
241
+ )
242
+ ).toEqual(new BigNumber(420));
243
+ });
244
+ });
245
+
246
+ describe("getTransactionCount", () => {
247
+ it("should return the expected payload", async () => {
248
+ expect(
249
+ await RPC_API.getTransactionCount(
250
+ fakeCurrency as CryptoCurrency,
251
+ "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d"
252
+ )
253
+ ).toEqual(5);
254
+ });
255
+ });
256
+
257
+ describe("getGasEstimation", () => {
258
+ it("should return the expected payload", async () => {
259
+ expect(
260
+ await RPC_API.getGasEstimation(account, {
261
+ recipient: "0x0000000000000000000000000000000000000000",
262
+ amount: new BigNumber(2),
263
+ gasLimit: new BigNumber(0),
264
+ gasPrice: new BigNumber(0),
265
+ data: Buffer.from(""),
266
+ type: 0,
267
+ } as EvmTransactionLegacy)
268
+ ).toEqual(new BigNumber(5));
269
+ });
270
+
271
+ it("should throw a GasEstimationError in case of error", async () => {
272
+ try {
273
+ await RPC_API.getGasEstimation(account, {
274
+ recipient: "wrongAddress",
275
+ amount: new BigNumber(2),
276
+ gasLimit: new BigNumber(0),
277
+ gasPrice: new BigNumber(0),
278
+ data: Buffer.from(""),
279
+ type: 0,
280
+ } as EvmTransactionLegacy);
281
+ fail("Promise should have been rejected");
282
+ } catch (e) {
283
+ if (e instanceof AssertionError) {
284
+ throw e;
285
+ }
286
+ expect(e).toBeInstanceOf(GasEstimationError);
287
+ }
288
+ });
289
+ });
290
+
291
+ describe("getFeesEstimation", () => {
292
+ it("should return the expected payload for an EIP1559 tx", async () => {
293
+ jest
294
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "send")
295
+ .mockImplementationOnce(async (method) => {
296
+ if (method === "eth_feeHistory") {
297
+ return {
298
+ reward: [
299
+ ["0x4a817c7ee", "0x4a817c7ee"],
300
+ ["0x773593f0", "0x773593f5"],
301
+ ["0x0", "0x0"],
302
+ ["0x773593f5", "0x773bae75"],
303
+ ],
304
+ baseFeePerGas: ["0x12", "0x10", "0x10", "0xe", "0xd"],
305
+ gasUsedRatio: [0.026089875, 0.406803, 0, 0.0866665],
306
+ };
307
+ }
308
+ });
309
+
310
+ expect(
311
+ await RPC_API.getFeesEstimation(fakeCurrency as CryptoCurrency)
312
+ ).toEqual({
313
+ maxFeePerGas: new BigNumber("6000000014"),
314
+ maxPriorityFeePerGas: new BigNumber("5999999988"),
315
+ gasPrice: null,
316
+ });
317
+ });
318
+
319
+ it("should return the expected payload for an EIP1559 tx when network returns 0 priority fee", async () => {
320
+ jest
321
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "send")
322
+ .mockImplementationOnce(async (method) => {
323
+ if (method === "eth_feeHistory") {
324
+ return {
325
+ reward: [
326
+ ["0x0", "0x0"],
327
+ ["0x0", "0x0"],
328
+ ["0x0", "0x0"],
329
+ ["0x0", "0x0"],
330
+ ],
331
+ baseFeePerGas: ["0x12", "0x10", "0x10", "0xe", "0xd"],
332
+ gasUsedRatio: [0.026089875, 0.406803, 0, 0.0866665],
333
+ };
334
+ }
335
+ });
336
+
337
+ expect(
338
+ await RPC_API.getFeesEstimation(fakeCurrency as CryptoCurrency)
339
+ ).toEqual({
340
+ maxFeePerGas: new BigNumber("1000000026"),
341
+ maxPriorityFeePerGas: new BigNumber(1e9),
342
+ gasPrice: null,
343
+ });
344
+ });
345
+
346
+ it("should return the expected payload for a legacy tx", async () => {
347
+ jest
348
+ .spyOn(ethers.providers.StaticJsonRpcProvider.prototype, "perform")
349
+ .mockImplementationOnce(async (method) => {
350
+ switch (method) {
351
+ case "getBlock":
352
+ return {
353
+ parentHash:
354
+ "0x474dee0136108e9412e9d84197b468bb057a8dad0f2024fc55adebc4a28fa8c5",
355
+ number: 1,
356
+ timestamp: 123,
357
+ difficulty: null,
358
+ gasLimit: ethers.BigNumber.from(1),
359
+ gasUsed: ethers.BigNumber.from(2),
360
+ extraData: "0x",
361
+ };
362
+ default:
363
+ throw new Error(`Method not mocked: ${method}`);
364
+ }
365
+ });
366
+
367
+ expect(
368
+ await RPC_API.getFeesEstimation({
369
+ ...fakeCurrency,
370
+ id: "optimism",
371
+ } as CryptoCurrency)
372
+ ).toEqual({
373
+ maxFeePerGas: null,
374
+ maxPriorityFeePerGas: null,
375
+ gasPrice: new BigNumber("666"),
376
+ });
377
+ });
378
+ });
379
+
380
+ describe("broadcastTransaction", () => {
381
+ it("should return the expected payload", async () => {
382
+ const serializedTransaction =
383
+ "0x02f873012d85010c388d0085077715912682520894c2907efcce4011c491bbeda8a0fa63ba7aab596c87038d7ea4c6800080c001a0bbffe7ba303ab03f697d64672c4a288ae863df8a62ffc67ba72872ce8c227f6fa01261e7c9f06af13631f03fad9b88d3c48931d353b6b41b4072fddcca5ec41629";
384
+ expect(
385
+ await RPC_API.broadcastTransaction(
386
+ fakeCurrency as CryptoCurrency,
387
+ serializedTransaction
388
+ )
389
+ ).toEqual({
390
+ ...ethers.utils.parseTransaction(serializedTransaction),
391
+ confirmations: 0,
392
+ wait: expect.any(Function),
393
+ });
394
+ });
395
+
396
+ it("should throw an insufficient funds errors", async () => {
397
+ const serializedTransaction =
398
+ "0x02f873012d85010c388d0085077715912682520894c2907efcce4011c491bbeda8a0fa63ba7aab596c87038d7ea4c6800080c001a0bbffe7ba303ab03f697d64672c4a288ae863df8a62ffc67ba72872ce8c227f6fa01261e7c9f06af13631f03fad9b88d3c48931d353b6b41b4072fddcca5ec41628";
399
+
400
+ try {
401
+ await RPC_API.broadcastTransaction(
402
+ fakeCurrency as CryptoCurrency,
403
+ serializedTransaction
404
+ );
405
+ fail("Promise should have been rejected");
406
+ } catch (e) {
407
+ if (e instanceof AssertionError) {
408
+ throw e;
409
+ }
410
+ expect(e).toBeInstanceOf(InsufficientFunds);
411
+ }
412
+ });
413
+
414
+ it("should throw any other error", async () => {
415
+ const serializedTransaction =
416
+ "0x02f873012d85010c388d0085077715912682520894c2907efcce4011c491bbeda8a0fa63ba7aab596c87038d7ea4c6800080c001a0bbffe7ba303ab03f697d64672c4a288ae863df8a62ffc67ba72872ce8c227f6fa01261e7c9f06af13631f03fad9b88d3c48931d353b6b41b4072fddcca5ec41625";
417
+
418
+ try {
419
+ await RPC_API.broadcastTransaction(
420
+ fakeCurrency as CryptoCurrency,
421
+ serializedTransaction
422
+ );
423
+ fail("Promise should have been rejected");
424
+ } catch (e) {
425
+ if (e instanceof AssertionError) {
426
+ throw e;
427
+ }
428
+ expect((e as Error).message).toEqual("any error");
429
+ }
430
+ });
431
+ });
432
+
433
+ describe("getBlock", () => {
434
+ it("should return the expected payload", async () => {
435
+ expect(await RPC_API.getBlock(fakeCurrency as CryptoCurrency, 0)).toEqual(
436
+ {
437
+ parentHash:
438
+ "0x474dee0136108e9412e9d84197b468bb057a8dad0f2024fc55adebc4a28fa8c5",
439
+ number: 1,
440
+ timestamp: 123,
441
+ difficulty: null,
442
+ _difficulty: null,
443
+ gasLimit: ethers.BigNumber.from(1),
444
+ gasUsed: ethers.BigNumber.from(2),
445
+ extraData: "0x",
446
+ baseFeePerGas: ethers.BigNumber.from(123),
447
+ }
448
+ );
449
+ });
450
+ });
451
+
452
+ describe("getSubAccount", () => {
453
+ it("should return the expected payload", async () => {
454
+ expect(
455
+ await RPC_API.getSubAccount(
456
+ fakeCurrency as CryptoCurrency,
457
+ account.freshAddress
458
+ )
459
+ ).toEqual({
460
+ balance: new BigNumber(420),
461
+ blockHeight: 69,
462
+ nonce: 5,
463
+ });
464
+ });
465
+ });
466
+
467
+ describe("getOptimismAdditionalFees", () => {
468
+ it("should return the expected payload", async () => {
469
+ RPC_API.getOptimismAdditionalFees.reset();
470
+ expect(
471
+ await RPC_API.getOptimismAdditionalFees(
472
+ { ...fakeCurrency, id: "optimism" } as CryptoCurrency,
473
+ {
474
+ mode: "send",
475
+ family: "evm",
476
+ recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c",
477
+ maxFeePerGas: new BigNumber("0x777159126"),
478
+ maxPriorityFeePerGas: new BigNumber("0x10c388d00"),
479
+ amount: new BigNumber("0x38d7ea4c68000"),
480
+ gasLimit: new BigNumber(0),
481
+ data: Buffer.from(""),
482
+ type: 2,
483
+ chainId: 1,
484
+ nonce: 52,
485
+ } as EvmTransaction
486
+ )
487
+ ).toEqual(new BigNumber(420));
488
+ });
489
+
490
+ it("should return 0 if the currency isn't optimism", async () => {
491
+ expect(
492
+ await RPC_API.getOptimismAdditionalFees(
493
+ fakeCurrency as CryptoCurrency,
494
+ {
495
+ mode: "send",
496
+ family: "evm",
497
+ recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c",
498
+ maxFeePerGas: new BigNumber("0x777159126"),
499
+ maxPriorityFeePerGas: new BigNumber("0x10c388d00"),
500
+ amount: new BigNumber("0x38d7ea4c68000"),
501
+ gasLimit: new BigNumber(0),
502
+ data: Buffer.from(""),
503
+ type: 2,
504
+ chainId: 1,
505
+ nonce: 52,
506
+ } as EvmTransaction
507
+ )
508
+ ).toEqual(new BigNumber(0));
509
+ });
510
+
511
+ it("should return 0 if the transaction is invalid", async () => {
512
+ expect(
513
+ await RPC_API.getOptimismAdditionalFees(
514
+ fakeCurrency as CryptoCurrency,
515
+ {
516
+ mode: "send",
517
+ family: "evm",
518
+ recipient: "", // no recipient for example
519
+ maxFeePerGas: new BigNumber("0x777159126"),
520
+ maxPriorityFeePerGas: new BigNumber("0x10c388d00"),
521
+ amount: new BigNumber("0x38d7ea4c68000"),
522
+ gasLimit: new BigNumber(0),
523
+ data: Buffer.from(""),
524
+ type: 2,
525
+ chainId: 1,
526
+ nonce: 52,
527
+ } as EvmTransaction
528
+ )
529
+ ).toEqual(new BigNumber(0));
530
+ });
531
+ });
532
+ });
@@ -0,0 +1,157 @@
1
+ import { DeviceCommunication } from "@ledgerhq/coin-framework/bridge/jsHelpers";
2
+ import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
3
+ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
+ import { Account } from "@ledgerhq/types-live";
5
+ import BigNumber from "bignumber.js";
6
+ import { buildSignOperation, applyEIP155 } from "../signOperation";
7
+ import { Transaction as EvmTransaction } from "../types";
8
+ import * as rpcAPI from "../api/rpc.common";
9
+ import { getEstimatedFees } from "../logic";
10
+ import { makeAccount } from "../testUtils";
11
+
12
+ const currency: CryptoCurrency = {
13
+ ...getCryptoCurrencyById("ethereum"),
14
+ ethereumLikeInfo: {
15
+ chainId: 1,
16
+ rpc: "my-rpc.com",
17
+ },
18
+ };
19
+ const account: Account = makeAccount(
20
+ "0x7265a60acAeaf3A5E18E10BC1128e72F27B2e176", // trump.eth
21
+ currency
22
+ );
23
+
24
+ const transactionEIP1559: EvmTransaction = {
25
+ amount: new BigNumber(100),
26
+ useAllAmount: false,
27
+ subAccountId: "id",
28
+ recipient: "0x6775e49108cb77cda06Fc3BEF51bcD497602aD88", // obama.eth
29
+ feesStrategy: "custom",
30
+ family: "evm",
31
+ mode: "send",
32
+ nonce: 0,
33
+ gasLimit: new BigNumber(21000),
34
+ chainId: 1,
35
+ maxFeePerGas: new BigNumber(100),
36
+ maxPriorityFeePerGas: new BigNumber(100),
37
+ type: 2,
38
+ };
39
+ const estimatedFees = getEstimatedFees(transactionEIP1559);
40
+
41
+ const mockWithDevice: DeviceCommunication =
42
+ () =>
43
+ (job: any): any =>
44
+ job({});
45
+
46
+ // Mocking here in order to be ack by the signOperation.ts file
47
+ jest.mock("@ledgerhq/hw-app-eth", () => ({
48
+ __esModule: true,
49
+ default: class {
50
+ signTransaction = () => ({
51
+ r: "123",
52
+ s: "abc",
53
+ v: "27",
54
+ });
55
+ },
56
+ ledgerService: {
57
+ resolveTransaction: () =>
58
+ Promise.resolve({
59
+ erc20Tokens: [],
60
+ nfts: [],
61
+ externalPlugin: [],
62
+ plugin: [],
63
+ }),
64
+ },
65
+ }));
66
+
67
+ describe("EVM Family", () => {
68
+ describe("signOperation.ts", () => {
69
+ describe("signOperation", () => {
70
+ beforeAll(() => {
71
+ jest
72
+ .spyOn(rpcAPI, "getTransactionCount")
73
+ .mockImplementation(async () => 1);
74
+ });
75
+
76
+ afterAll(() => {
77
+ jest.restoreAllMocks();
78
+ });
79
+
80
+ it("should return an optimistic operation and a signed hash based on hardware ECDSA signatures returned by the app bindings", (done) => {
81
+ const signOperation = buildSignOperation(mockWithDevice);
82
+
83
+ const signOpObservable = signOperation({
84
+ account,
85
+ transaction: transactionEIP1559,
86
+ deviceId: "",
87
+ });
88
+
89
+ signOpObservable.subscribe((obs) => {
90
+ if (obs.type === "signed") {
91
+ const {
92
+ signedOperation: { signature, operation },
93
+ } = obs;
94
+
95
+ expect(operation).toEqual({
96
+ id: "js:2:ethereum:0x7265a60acAeaf3A5E18E10BC1128e72F27B2e176:--OUT",
97
+ hash: "",
98
+ type: "OUT",
99
+ value: new BigNumber(100).plus(estimatedFees),
100
+ fee: estimatedFees,
101
+ blockHash: null,
102
+ blockHeight: null,
103
+ senders: [account.freshAddress],
104
+ recipients: [transactionEIP1559.recipient],
105
+ accountId: account.id,
106
+ transactionSequenceNumber: 1,
107
+ date: expect.any(Date),
108
+ extra: {},
109
+ });
110
+ expect(signature).toBe(
111
+ "0x02e601016464825208946775e49108cb77cda06fc3bef51bcd497602ad886480c080820123820abc"
112
+ );
113
+ done();
114
+ }
115
+ });
116
+ });
117
+ });
118
+
119
+ describe("applyEIP155", () => {
120
+ const chainIds = [
121
+ 1, //ethereum
122
+ 5, // goerli
123
+ 10, // optimism
124
+ 14, // flare
125
+ 19, // songbird
126
+ 56, // bsc
127
+ 137, // polygon
128
+ 250, // fantom
129
+ 1284, // moonbeam
130
+ ];
131
+ const possibleHexV = [
132
+ "00", // 0 - ethereum + testnets should always retrun 0/1 from hw-app-eth
133
+ "01", // 1
134
+ "1b", // 27 - type 0 transactions from other chains (when chain id > 109) should always return 27/28
135
+ "1c", // 28
136
+ ];
137
+
138
+ chainIds.forEach((chainId) => {
139
+ possibleHexV.forEach((v) => {
140
+ it(`should return an EIP155 compatible v for chain id ${chainId} with v = ${parseInt(
141
+ v,
142
+ 16
143
+ )}`, () => {
144
+ const eip155Logic = chainId * 2 + 35;
145
+ expect(
146
+ [eip155Logic, eip155Logic + 1] // eip155 + parity
147
+ ).toContain(applyEIP155(v, chainId));
148
+ });
149
+ });
150
+
151
+ it("should return the value given by the nano as is if we can't figure out parity from it", () => {
152
+ expect(applyEIP155("1b39", chainId)).toBe(6969);
153
+ });
154
+ });
155
+ });
156
+ });
157
+ });