@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.
- package/.eslintrc.js +57 -0
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +18 -0
- package/jest.config.js +6 -0
- package/package.json +102 -0
- package/src/__tests__/adapters.unit.test.ts +527 -0
- package/src/__tests__/broadcast.unit.test.ts +181 -0
- package/src/__tests__/buildOptimisticOperation.unit.test.ts +182 -0
- package/src/__tests__/createTransaction.unit.test.ts +52 -0
- package/src/__tests__/deviceTransactionConfig.unit.test.ts +245 -0
- package/src/__tests__/estimateMaxSpendable.unit.test.ts +123 -0
- package/src/__tests__/getTransactionStatus.unit.test.ts +355 -0
- package/src/__tests__/hw-getAddress.unit.test.ts +24 -0
- package/src/__tests__/logic.unit.test.ts +406 -0
- package/src/__tests__/preload.unit.test.ts +139 -0
- package/src/__tests__/prepareTransaction.unit.test.ts +394 -0
- package/src/__tests__/rpc.unit.test.ts +532 -0
- package/src/__tests__/signOperation.unit.test.ts +157 -0
- package/src/__tests__/synchronization.unit.test.ts +832 -0
- package/src/__tests__/transaction.unit.test.ts +196 -0
- package/src/abis/erc20.abi.json +230 -0
- package/src/abis/optimismGasPriceOracle.abi.json +252 -0
- package/src/adapters.ts +148 -0
- package/src/api/etherscan.ts +124 -0
- package/src/api/rpc.common.ts +354 -0
- package/src/api/rpc.native.ts +5 -0
- package/src/api/rpc.ts +2 -0
- package/src/bridge/js.ts +77 -0
- package/src/bridge.integration.test.ts +93 -0
- package/src/broadcast.ts +40 -0
- package/src/buildOptimisticOperation.ts +113 -0
- package/src/cli-transaction.ts +11 -0
- package/src/createTransaction.ts +25 -0
- package/src/datasets/ethereum.scanAccounts.1.ts +48 -0
- package/src/datasets/ethereum1.ts +20 -0
- package/src/datasets/ethereum2.ts +20 -0
- package/src/datasets/ethereum_classic.ts +68 -0
- package/src/deviceTransactionConfig.ts +64 -0
- package/src/errors.ts +5 -0
- package/src/estimateMaxSpendable.ts +19 -0
- package/src/getTransactionStatus.ts +186 -0
- package/src/hw-getAddress.ts +24 -0
- package/src/logic.ts +149 -0
- package/src/preload.ts +54 -0
- package/src/prepareTransaction.ts +176 -0
- package/src/signOperation.ts +127 -0
- package/src/specs.ts +344 -0
- package/src/speculos-deviceActions.ts +83 -0
- package/src/synchronization.ts +317 -0
- package/src/testUtils.ts +153 -0
- package/src/transaction.ts +193 -0
- package/src/types.ts +132 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { getCryptoCurrencyById, getTokenById } from "@ledgerhq/cryptoassets";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
3
|
+
import { ethers } from "ethers";
|
|
4
|
+
import ERC20ABI from "../abis/erc20.abi.json";
|
|
5
|
+
import * as rpcAPI from "../api/rpc.common";
|
|
6
|
+
import {
|
|
7
|
+
prepareForSignOperation,
|
|
8
|
+
prepareTransaction,
|
|
9
|
+
} from "../prepareTransaction";
|
|
10
|
+
import { makeAccount, makeTokenAccount } from "../testUtils";
|
|
11
|
+
import { Transaction as EvmTransaction } from "../types";
|
|
12
|
+
|
|
13
|
+
const currency = getCryptoCurrencyById("ethereum");
|
|
14
|
+
const tokenAccount = makeTokenAccount(
|
|
15
|
+
"0xkvn",
|
|
16
|
+
getTokenById("ethereum/erc20/usd__coin")
|
|
17
|
+
);
|
|
18
|
+
const account = makeAccount("0xkvn", currency, [tokenAccount]);
|
|
19
|
+
const transaction: EvmTransaction = {
|
|
20
|
+
amount: new BigNumber(100),
|
|
21
|
+
useAllAmount: false,
|
|
22
|
+
subAccountId: "id",
|
|
23
|
+
recipient: "0x6bfD74C0996F269Bcece59191EFf667b3dFD73b9",
|
|
24
|
+
feesStrategy: "custom",
|
|
25
|
+
family: "evm",
|
|
26
|
+
mode: "send",
|
|
27
|
+
gasPrice: new BigNumber(0),
|
|
28
|
+
gasLimit: new BigNumber(21000),
|
|
29
|
+
nonce: 0,
|
|
30
|
+
chainId: 1,
|
|
31
|
+
};
|
|
32
|
+
const tokenTransaction: EvmTransaction = {
|
|
33
|
+
...transaction,
|
|
34
|
+
subAccountId: tokenAccount.id,
|
|
35
|
+
};
|
|
36
|
+
const expectedData = (recipient: string, amount: BigNumber): Buffer =>
|
|
37
|
+
Buffer.from(
|
|
38
|
+
new ethers.utils.Interface(ERC20ABI)
|
|
39
|
+
.encodeFunctionData("transfer", [recipient, amount.toFixed()])
|
|
40
|
+
.slice(2),
|
|
41
|
+
"hex"
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
describe("EVM Family", () => {
|
|
45
|
+
describe("prepareTransaction.ts", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// These mocks will be overriden in some tests
|
|
48
|
+
jest
|
|
49
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
50
|
+
.mockImplementation(async () => new BigNumber(21000));
|
|
51
|
+
// These mocks will be overriden in some tests
|
|
52
|
+
jest.spyOn(rpcAPI, "getFeesEstimation").mockImplementation(async () => ({
|
|
53
|
+
gasPrice: new BigNumber(1),
|
|
54
|
+
maxFeePerGas: new BigNumber(1),
|
|
55
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
jest.restoreAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("prepareTransaction", () => {
|
|
64
|
+
describe("Coins", () => {
|
|
65
|
+
it("should have a gasLimit = 0 when recipient has an error", async () => {
|
|
66
|
+
jest
|
|
67
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
68
|
+
.mockImplementation(async () => {
|
|
69
|
+
throw new Error();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const tx = await prepareTransaction(account, {
|
|
73
|
+
...transaction,
|
|
74
|
+
recipient: "notValid",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(tx).toEqual({
|
|
78
|
+
...transaction,
|
|
79
|
+
recipient: "notValid",
|
|
80
|
+
maxFeePerGas: new BigNumber(1),
|
|
81
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
82
|
+
gasPrice: undefined,
|
|
83
|
+
gasLimit: new BigNumber(0),
|
|
84
|
+
type: 2,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should have a gasLimit = 0 when amount has an error", async () => {
|
|
89
|
+
jest
|
|
90
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
91
|
+
.mockImplementation(async () => {
|
|
92
|
+
throw new Error();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const tx = await prepareTransaction(account, {
|
|
96
|
+
...transaction,
|
|
97
|
+
amount: new BigNumber(0),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(tx).toEqual({
|
|
101
|
+
...transaction,
|
|
102
|
+
amount: new BigNumber(0),
|
|
103
|
+
maxFeePerGas: new BigNumber(1),
|
|
104
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
105
|
+
gasPrice: undefined,
|
|
106
|
+
gasLimit: new BigNumber(0),
|
|
107
|
+
type: 2,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should return an EIP1559 coin transaction", async () => {
|
|
112
|
+
const tx = await prepareTransaction(account, transaction);
|
|
113
|
+
|
|
114
|
+
expect(tx).toEqual({
|
|
115
|
+
...transaction,
|
|
116
|
+
maxFeePerGas: new BigNumber(1),
|
|
117
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
118
|
+
type: 2,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return a legacy coin transaction", async () => {
|
|
123
|
+
jest
|
|
124
|
+
.spyOn(rpcAPI, "getFeesEstimation")
|
|
125
|
+
.mockImplementationOnce(async () => ({
|
|
126
|
+
gasPrice: new BigNumber(1),
|
|
127
|
+
maxFeePerGas: null,
|
|
128
|
+
maxPriorityFeePerGas: null,
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const tx = await prepareTransaction(account, transaction);
|
|
132
|
+
|
|
133
|
+
expect(tx).toEqual({
|
|
134
|
+
...transaction,
|
|
135
|
+
gasPrice: new BigNumber(1),
|
|
136
|
+
type: 0,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should create a coin transaction using all amount in the account", async () => {
|
|
141
|
+
const accountWithBalance = {
|
|
142
|
+
...account,
|
|
143
|
+
balance: new BigNumber(4206900),
|
|
144
|
+
};
|
|
145
|
+
const transactionWithUseAllAmount = {
|
|
146
|
+
...transaction,
|
|
147
|
+
useAllAmount: true,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const tx = await prepareTransaction(
|
|
151
|
+
accountWithBalance,
|
|
152
|
+
transactionWithUseAllAmount
|
|
153
|
+
);
|
|
154
|
+
const estimatedFees = new BigNumber(21000); // 21000 gasLimit * 1 maxFeePerGas
|
|
155
|
+
|
|
156
|
+
expect(tx).toEqual({
|
|
157
|
+
...transactionWithUseAllAmount,
|
|
158
|
+
amount: accountWithBalance.balance.minus(estimatedFees),
|
|
159
|
+
maxFeePerGas: new BigNumber(1),
|
|
160
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
161
|
+
type: 2,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should do a gas estimation when data has been added to the coin transaction", async () => {
|
|
166
|
+
jest
|
|
167
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
168
|
+
.mockImplementationOnce(async () => new BigNumber(12));
|
|
169
|
+
|
|
170
|
+
const accountWithBalance = {
|
|
171
|
+
...account,
|
|
172
|
+
balance: new BigNumber(4206900),
|
|
173
|
+
};
|
|
174
|
+
const tx = await prepareTransaction(accountWithBalance, {
|
|
175
|
+
...transaction,
|
|
176
|
+
data: Buffer.from("Sm4rTC0ntr4ct", "hex"),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(tx).toEqual({
|
|
180
|
+
...transaction,
|
|
181
|
+
data: Buffer.from("Sm4rTC0ntr4ct", "hex"),
|
|
182
|
+
maxFeePerGas: new BigNumber(1),
|
|
183
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
184
|
+
gasLimit: new BigNumber(12),
|
|
185
|
+
type: 2,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("Tokens", () => {
|
|
191
|
+
it("should have a gasLimit = 0 and no data when recipient has an error", async () => {
|
|
192
|
+
jest
|
|
193
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
194
|
+
.mockImplementation(async () => {
|
|
195
|
+
throw new Error();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const tx = await prepareTransaction(account, {
|
|
199
|
+
...tokenTransaction,
|
|
200
|
+
recipient: "notValid",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(tx).toEqual({
|
|
204
|
+
...tokenTransaction,
|
|
205
|
+
recipient: "notValid",
|
|
206
|
+
maxFeePerGas: new BigNumber(1),
|
|
207
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
208
|
+
gasPrice: undefined,
|
|
209
|
+
gasLimit: new BigNumber(0),
|
|
210
|
+
data: undefined,
|
|
211
|
+
type: 2,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should have a gasLimit = 0 when amount has an error", async () => {
|
|
216
|
+
jest
|
|
217
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
218
|
+
.mockImplementation(async () => {
|
|
219
|
+
throw new Error();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const tx = await prepareTransaction(account, {
|
|
223
|
+
...tokenTransaction,
|
|
224
|
+
amount: new BigNumber(0),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(tx).toEqual({
|
|
228
|
+
...tokenTransaction,
|
|
229
|
+
amount: new BigNumber(0),
|
|
230
|
+
maxFeePerGas: new BigNumber(1),
|
|
231
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
232
|
+
gasPrice: undefined,
|
|
233
|
+
gasLimit: new BigNumber(0),
|
|
234
|
+
data: expectedData(tokenTransaction.recipient, new BigNumber(0)),
|
|
235
|
+
type: 2,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should create a token transaction using all amount in the token account", async () => {
|
|
240
|
+
jest
|
|
241
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
242
|
+
.mockImplementationOnce(async () => new BigNumber(12));
|
|
243
|
+
|
|
244
|
+
const tokenAccountWithBalance = {
|
|
245
|
+
...tokenAccount,
|
|
246
|
+
balance: new BigNumber(200),
|
|
247
|
+
};
|
|
248
|
+
const account2 = {
|
|
249
|
+
...account,
|
|
250
|
+
subAccounts: [tokenAccountWithBalance],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const tx = await prepareTransaction(account2, {
|
|
254
|
+
...tokenTransaction,
|
|
255
|
+
amount: new BigNumber(0),
|
|
256
|
+
useAllAmount: true,
|
|
257
|
+
subAccountId: tokenAccountWithBalance.id,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(tx).toEqual({
|
|
261
|
+
...tokenTransaction,
|
|
262
|
+
amount: tokenAccountWithBalance.balance,
|
|
263
|
+
useAllAmount: true,
|
|
264
|
+
subAccountId: tokenAccountWithBalance.id,
|
|
265
|
+
data: expectedData(
|
|
266
|
+
tokenTransaction.recipient,
|
|
267
|
+
tokenAccountWithBalance.balance
|
|
268
|
+
),
|
|
269
|
+
maxFeePerGas: new BigNumber(1),
|
|
270
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
271
|
+
gasPrice: undefined,
|
|
272
|
+
gasLimit: new BigNumber(12),
|
|
273
|
+
type: 2,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should go to gas estimation with a transaction without 0 amount", async () => {
|
|
278
|
+
jest
|
|
279
|
+
.spyOn(rpcAPI, "getGasEstimation")
|
|
280
|
+
.mockImplementationOnce(async () => new BigNumber(12));
|
|
281
|
+
|
|
282
|
+
const tokenAccountWithBalance = {
|
|
283
|
+
...tokenAccount,
|
|
284
|
+
balance: new BigNumber(200),
|
|
285
|
+
};
|
|
286
|
+
const account2 = {
|
|
287
|
+
...account,
|
|
288
|
+
subAccounts: [tokenAccountWithBalance],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
await prepareTransaction(account2, {
|
|
292
|
+
...tokenTransaction,
|
|
293
|
+
useAllAmount: true,
|
|
294
|
+
subAccountId: tokenAccountWithBalance.id,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(rpcAPI.getGasEstimation).toBeCalledWith(
|
|
298
|
+
account2,
|
|
299
|
+
expect.objectContaining({
|
|
300
|
+
recipient: tokenAccount.token.contractAddress,
|
|
301
|
+
amount: new BigNumber(0),
|
|
302
|
+
data: expectedData(
|
|
303
|
+
tokenTransaction.recipient,
|
|
304
|
+
tokenAccountWithBalance.balance
|
|
305
|
+
),
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should return an EIP1559 token transaction", async () => {
|
|
311
|
+
const tokenAccountWithBalance = {
|
|
312
|
+
...tokenAccount,
|
|
313
|
+
balance: new BigNumber(200),
|
|
314
|
+
};
|
|
315
|
+
const account2 = {
|
|
316
|
+
...account,
|
|
317
|
+
subAccounts: [tokenAccountWithBalance],
|
|
318
|
+
};
|
|
319
|
+
const tx = await prepareTransaction(account2, tokenTransaction);
|
|
320
|
+
|
|
321
|
+
expect(tx).toEqual({
|
|
322
|
+
...tokenTransaction,
|
|
323
|
+
data: expectedData(
|
|
324
|
+
tokenTransaction.recipient,
|
|
325
|
+
tokenTransaction.amount
|
|
326
|
+
),
|
|
327
|
+
maxFeePerGas: new BigNumber(1),
|
|
328
|
+
maxPriorityFeePerGas: new BigNumber(1),
|
|
329
|
+
type: 2,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should return a legacy token transaction", async () => {
|
|
334
|
+
jest
|
|
335
|
+
.spyOn(rpcAPI, "getFeesEstimation")
|
|
336
|
+
.mockImplementationOnce(async () => ({
|
|
337
|
+
gasPrice: new BigNumber(1),
|
|
338
|
+
maxFeePerGas: null,
|
|
339
|
+
maxPriorityFeePerGas: null,
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
const tokenAccountWithBalance = {
|
|
343
|
+
...tokenAccount,
|
|
344
|
+
balance: new BigNumber(200),
|
|
345
|
+
};
|
|
346
|
+
const account2 = {
|
|
347
|
+
...account,
|
|
348
|
+
subAccounts: [tokenAccountWithBalance],
|
|
349
|
+
};
|
|
350
|
+
const tx = await prepareTransaction(account2, tokenTransaction);
|
|
351
|
+
|
|
352
|
+
expect(tx).toEqual({
|
|
353
|
+
...tokenTransaction,
|
|
354
|
+
data: expectedData(
|
|
355
|
+
tokenTransaction.recipient,
|
|
356
|
+
tokenTransaction.amount
|
|
357
|
+
),
|
|
358
|
+
gasPrice: new BigNumber(1),
|
|
359
|
+
type: 0,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("prepareForSignOperation", () => {
|
|
366
|
+
beforeEach(() => {
|
|
367
|
+
jest
|
|
368
|
+
.spyOn(rpcAPI, "getTransactionCount")
|
|
369
|
+
.mockImplementation(() => Promise.resolve(10));
|
|
370
|
+
});
|
|
371
|
+
afterEach(() => {
|
|
372
|
+
jest.restoreAllMocks();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should not change a coin transaction", async () => {
|
|
376
|
+
expect(await prepareForSignOperation(account, transaction)).toEqual({
|
|
377
|
+
...transaction,
|
|
378
|
+
nonce: 10,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("should update a token transaction with the correct recipient", async () => {
|
|
383
|
+
expect(
|
|
384
|
+
await prepareForSignOperation(account, tokenTransaction)
|
|
385
|
+
).toEqual({
|
|
386
|
+
...tokenTransaction,
|
|
387
|
+
amount: new BigNumber(0),
|
|
388
|
+
recipient: tokenAccount.token.contractAddress,
|
|
389
|
+
nonce: 10,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|