@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,406 @@
1
+ import { getCryptoCurrencyById, getTokenById } from "@ledgerhq/cryptoassets";
2
+ import * as cryptoAssetsTokens from "@ledgerhq/cryptoassets/tokens";
3
+ import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
4
+ import BigNumber from "bignumber.js";
5
+ import * as RPC_API from "../api/rpc.common";
6
+ import {
7
+ eip1559TransactionHasFees,
8
+ getAdditionalLayer2Fees,
9
+ getEstimatedFees,
10
+ getSyncHash,
11
+ legacyTransactionHasFees,
12
+ mergeSubAccounts,
13
+ } from "../logic";
14
+ import { makeAccount, makeOperation, makeTokenAccount } from "../testUtils";
15
+ import { EvmTransactionEIP1559, EvmTransactionLegacy } from "../types";
16
+
17
+ describe("EVM Family", () => {
18
+ describe("logic.ts", () => {
19
+ describe("legacyTransactionHasFees", () => {
20
+ it("should return true for legacy tx with fees", () => {
21
+ const tx: Partial<EvmTransactionLegacy> = {
22
+ type: 0,
23
+ gasPrice: new BigNumber(100),
24
+ };
25
+
26
+ expect(legacyTransactionHasFees(tx as EvmTransactionLegacy)).toBe(true);
27
+ });
28
+
29
+ it("should return true for type 1 (unused in the live for now) tx with fees", () => {
30
+ const tx: Partial<EvmTransactionLegacy> = {
31
+ type: 1,
32
+ gasPrice: new BigNumber(100),
33
+ };
34
+
35
+ expect(legacyTransactionHasFees(tx as EvmTransactionLegacy)).toBe(true);
36
+ });
37
+
38
+ it("should return false for legacy tx without fees", () => {
39
+ const tx: Partial<EvmTransactionLegacy> = {
40
+ type: 0,
41
+ };
42
+
43
+ expect(legacyTransactionHasFees(tx as any)).toBe(false);
44
+ });
45
+
46
+ it("should return false for legacy tx with wrong fees", () => {
47
+ const tx: Partial<EvmTransactionEIP1559> = {
48
+ type: 2,
49
+ maxFeePerGas: new BigNumber(100),
50
+ maxPriorityFeePerGas: new BigNumber(100),
51
+ };
52
+
53
+ expect(legacyTransactionHasFees(tx as any)).toBe(false);
54
+ });
55
+
56
+ it("should return true for legacy tx with fees but no type (default being a legacy tx)", () => {
57
+ const tx: Partial<EvmTransactionLegacy> = {
58
+ gasPrice: new BigNumber(100),
59
+ };
60
+
61
+ expect(legacyTransactionHasFees(tx as any)).toBe(true);
62
+ });
63
+ });
64
+
65
+ describe("eip1559TransactionHasFess", () => {
66
+ it("should return true for 1559 tx with fees", () => {
67
+ const tx: Partial<EvmTransactionEIP1559> = {
68
+ type: 2,
69
+ maxFeePerGas: new BigNumber(100),
70
+ maxPriorityFeePerGas: new BigNumber(100),
71
+ };
72
+
73
+ expect(eip1559TransactionHasFees(tx as any)).toBe(true);
74
+ });
75
+
76
+ it("should return false for 1559 tx without fees", () => {
77
+ const tx: Partial<EvmTransactionEIP1559> = {
78
+ type: 2,
79
+ };
80
+
81
+ expect(eip1559TransactionHasFees(tx as any)).toBe(false);
82
+ });
83
+
84
+ it("should return false for 1559 tx with wrong fees", () => {
85
+ const tx: unknown = {
86
+ type: 2,
87
+ gasPrice: new BigNumber(100),
88
+ };
89
+
90
+ expect(eip1559TransactionHasFees(tx as any)).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe("getEstimatedFees", () => {
95
+ it("should return the right fee estimation for a legacy tx", () => {
96
+ const tx = {
97
+ type: 0,
98
+ gasLimit: new BigNumber(3),
99
+ gasPrice: new BigNumber(23),
100
+ maxFeePerGas: new BigNumber(100),
101
+ maxPriorityFeePerGas: new BigNumber(40),
102
+ };
103
+
104
+ expect(getEstimatedFees(tx as any)).toEqual(new BigNumber(69));
105
+ });
106
+
107
+ it("should fallback with tx without type", () => {
108
+ const tx = {};
109
+ expect(getEstimatedFees(tx as any)).toEqual(new BigNumber(0));
110
+ });
111
+
112
+ it("should fallback with badly formatted legacy tx", () => {
113
+ const tx = {
114
+ type: 0,
115
+ };
116
+
117
+ expect(getEstimatedFees(tx as any)).toEqual(new BigNumber(0));
118
+ });
119
+
120
+ it("should return the right fee estimation for a 1559 tx", () => {
121
+ const tx = {
122
+ type: 2,
123
+ gasLimit: new BigNumber(42),
124
+ gasPrice: new BigNumber(23),
125
+ maxFeePerGas: new BigNumber(10),
126
+ maxPriorityFeePerGas: new BigNumber(40),
127
+ };
128
+
129
+ expect(getEstimatedFees(tx as any)).toEqual(new BigNumber(420));
130
+ });
131
+
132
+ it("should fallback with badly formatted 1559 tx", () => {
133
+ const tx = {
134
+ type: 2,
135
+ };
136
+
137
+ expect(getEstimatedFees(tx as any)).toEqual(new BigNumber(0));
138
+ });
139
+ });
140
+
141
+ describe("getAdditionalLayer2Fees", () => {
142
+ const optimism = getCryptoCurrencyById("optimism");
143
+ const ethereum = getCryptoCurrencyById("ethereum");
144
+
145
+ beforeEach(() => {
146
+ jest.clearAllMocks();
147
+ });
148
+
149
+ it("should try to get additionalFees for a valid layer 2", async () => {
150
+ const spy = jest
151
+ .spyOn(RPC_API, "getOptimismAdditionalFees")
152
+ .mockImplementation(jest.fn());
153
+
154
+ await getAdditionalLayer2Fees(optimism, {} as any);
155
+ expect(spy).toBeCalled();
156
+ });
157
+
158
+ it("should not try to get additionalFees for an invalid layer 2", async () => {
159
+ const spy = jest
160
+ .spyOn(RPC_API, "getOptimismAdditionalFees")
161
+ .mockImplementation(jest.fn());
162
+
163
+ await getAdditionalLayer2Fees(ethereum, {} as any);
164
+ expect(spy).not.toBeCalled();
165
+ });
166
+ });
167
+
168
+ describe("mergeSubAccounts", () => {
169
+ it("should merge 2 different sub accounts", () => {
170
+ const tokenAccount1 = {
171
+ ...makeTokenAccount(
172
+ "0xkvn",
173
+ getTokenById("ethereum/erc20/usd__coin")
174
+ ),
175
+ balance: new BigNumber(1),
176
+ operations: [],
177
+ };
178
+ const tokenAccount2 = {
179
+ ...makeTokenAccount("0xkvn", getTokenById("ethereum/erc20/weth")),
180
+ balance: new BigNumber(2),
181
+ operations: [],
182
+ };
183
+ const account = makeAccount(
184
+ "0xkvn",
185
+ getCryptoCurrencyById("ethereum"),
186
+ [tokenAccount1]
187
+ );
188
+
189
+ const newSubAccounts = mergeSubAccounts(account, [tokenAccount2]);
190
+ expect(newSubAccounts).toEqual([tokenAccount1, tokenAccount2]);
191
+ expect(newSubAccounts).not.toBe(account.subAccounts); // shouldn't mutate original account
192
+ expect(account.subAccounts).toEqual([tokenAccount1]); // shouldn't mutate original account
193
+ expect(newSubAccounts[0]).toBe(account.subAccounts?.[0]); // keeping the reference though
194
+ });
195
+
196
+ it("should merge 2 different sub accounts and update the first one", () => {
197
+ const tokenAccount1 = {
198
+ ...makeTokenAccount(
199
+ "0xkvn",
200
+ getTokenById("ethereum/erc20/usd__coin")
201
+ ),
202
+ balance: new BigNumber(1),
203
+ operations: [],
204
+ };
205
+ const tokenAccount1Bis = {
206
+ ...tokenAccount1,
207
+ balance: new BigNumber(10),
208
+ spendableBalance: new BigNumber(11),
209
+ operationsCount: 0,
210
+ balanceHistoryCache: {
211
+ HOUR: {
212
+ latestDate: 123,
213
+ balances: [123],
214
+ },
215
+ DAY: {
216
+ latestDate: 234,
217
+ balances: [234],
218
+ },
219
+ WEEK: {
220
+ latestDate: 345,
221
+ balances: [345],
222
+ },
223
+ },
224
+ operations: [],
225
+ };
226
+ const tokenAccount2 = {
227
+ ...makeTokenAccount("0xkvn", getTokenById("ethereum/erc20/weth")),
228
+ balance: new BigNumber(2),
229
+ operations: [],
230
+ };
231
+ const account = makeAccount(
232
+ "0xkvn",
233
+ getCryptoCurrencyById("ethereum"),
234
+ [tokenAccount1]
235
+ );
236
+
237
+ const newSubAccounts = mergeSubAccounts(account, [
238
+ tokenAccount1Bis,
239
+ tokenAccount2,
240
+ ]);
241
+ expect(newSubAccounts).toEqual([tokenAccount1Bis, tokenAccount2]);
242
+ expect(newSubAccounts).not.toBe(account.subAccounts); // shouldn't mutate original account
243
+ expect(account.subAccounts).toEqual([tokenAccount1]); // shouldn't mutate original account
244
+ expect(newSubAccounts[0]).not.toBe(account.subAccounts?.[0]); // changing the ref as a change happened in tokenAccount1
245
+ });
246
+
247
+ it("should update subAccount ops", () => {
248
+ const op1 = makeOperation();
249
+ const op2 = makeOperation({
250
+ hash: "0xdiffHash",
251
+ });
252
+ const op3 = makeOperation({
253
+ hash: "0xAgAinAnotHeRH4sh",
254
+ });
255
+ const tokenAccount1 = {
256
+ ...makeTokenAccount(
257
+ "0xkvn",
258
+ getTokenById("ethereum/erc20/usd__coin")
259
+ ),
260
+ balance: new BigNumber(1),
261
+ operations: [op1, op2],
262
+ operationsCount: 2,
263
+ };
264
+ const tokenAccount1Bis = {
265
+ ...tokenAccount1,
266
+ operations: [op3, op1, op2],
267
+ operationsCount: 3,
268
+ };
269
+ const account = makeAccount(
270
+ "0xkvn",
271
+ getCryptoCurrencyById("ethereum"),
272
+ [tokenAccount1]
273
+ );
274
+
275
+ const newSubAccounts = mergeSubAccounts(account, [tokenAccount1Bis]);
276
+ expect(newSubAccounts).not.toBe(account.subAccounts); // shouldn't mutate original account
277
+ expect(account.subAccounts).toEqual([tokenAccount1]); // shouldn't mutate original account
278
+ expect(newSubAccounts[0]).not.toBe(account.subAccounts?.[0]); // changing the ref as change happened
279
+ expect(newSubAccounts[0]?.operations?.[1]).toBe(
280
+ account.subAccounts?.[0]?.operations?.[0]
281
+ ); // keeping the reference for the ops though
282
+ expect(newSubAccounts[0]?.operations?.[2]).toBe(
283
+ account.subAccounts?.[0]?.operations?.[1]
284
+ ); // keeping the reference for the ops though
285
+ expect(newSubAccounts).toEqual([tokenAccount1Bis]);
286
+ });
287
+
288
+ it("should return only new sub accounts", () => {
289
+ const tokenAccount = {
290
+ ...makeTokenAccount(
291
+ "0xkvn",
292
+ getTokenById("ethereum/erc20/usd__coin")
293
+ ),
294
+ balance: new BigNumber(1),
295
+ };
296
+ const account = makeAccount("0xkvn", getCryptoCurrencyById("ethereum"));
297
+ delete account.subAccounts;
298
+
299
+ const newSubAccounts = mergeSubAccounts(account, [tokenAccount]);
300
+ expect(newSubAccounts).toEqual([tokenAccount]);
301
+ expect(account.subAccounts).toBe(undefined); // shouldn't mutate original account
302
+ });
303
+
304
+ it("should dedup sub accounts", () => {
305
+ const tokenAccount = {
306
+ ...makeTokenAccount(
307
+ "0xkvn",
308
+ getTokenById("ethereum/erc20/usd__coin")
309
+ ),
310
+ balance: new BigNumber(1),
311
+ };
312
+ const account = makeAccount(
313
+ "0xkvn",
314
+ getCryptoCurrencyById("ethereum"),
315
+ [tokenAccount]
316
+ );
317
+
318
+ const newSubAccounts = mergeSubAccounts(account, [
319
+ tokenAccount,
320
+ { ...tokenAccount },
321
+ { ...tokenAccount },
322
+ ]);
323
+ expect(newSubAccounts).toEqual([tokenAccount]);
324
+ });
325
+ });
326
+
327
+ describe("getSyncHash", () => {
328
+ const currency = getCryptoCurrencyById("ethereum");
329
+
330
+ afterEach(() => {
331
+ jest.restoreAllMocks();
332
+ });
333
+
334
+ it("should provide a valid sha256 hash", () => {
335
+ expect(getSyncHash(currency)).toStrictEqual(
336
+ expect.stringMatching(/^0x[A-Fa-f0-9]{64}$/)
337
+ );
338
+ });
339
+
340
+ it("should provide a hash not dependent on reference", () => {
341
+ jest
342
+ .spyOn(cryptoAssetsTokens, "listTokensForCryptoCurrency")
343
+ .mockImplementationOnce((currency): TokenCurrency[] => {
344
+ const { listTokensForCryptoCurrency } = jest.requireActual(
345
+ "@ledgerhq/cryptoassets/tokens"
346
+ );
347
+ return listTokensForCryptoCurrency(currency).map(
348
+ (t: TokenCurrency) => ({ ...t })
349
+ );
350
+ });
351
+ expect(getSyncHash(currency)).toEqual(getSyncHash(currency));
352
+ });
353
+
354
+ it("should provide a new hash if a token is removed", () => {
355
+ jest
356
+ .spyOn(cryptoAssetsTokens, "listTokensForCryptoCurrency")
357
+ .mockImplementationOnce((currency) => {
358
+ const { listTokensForCryptoCurrency } = jest.requireActual(
359
+ "@ledgerhq/cryptoassets/tokens"
360
+ );
361
+ const list: TokenCurrency[] = listTokensForCryptoCurrency(currency);
362
+ return list.slice(0, list.length - 2);
363
+ });
364
+ expect(getSyncHash(currency)).not.toEqual(getSyncHash(currency));
365
+ });
366
+
367
+ it("should provide a new hash if a token is modified", () => {
368
+ jest
369
+ .spyOn(cryptoAssetsTokens, "listTokensForCryptoCurrency")
370
+ .mockImplementationOnce((currency) => {
371
+ const { listTokensForCryptoCurrency } = jest.requireActual(
372
+ "@ledgerhq/cryptoassets/tokens"
373
+ );
374
+ const [first, ...rest]: TokenCurrency[] =
375
+ listTokensForCryptoCurrency(currency);
376
+ const modifedFirst = { ...first, delisted: !first.delisted };
377
+ return [modifedFirst, ...rest];
378
+ });
379
+
380
+ expect(getSyncHash(currency)).not.toEqual(getSyncHash(currency));
381
+ });
382
+
383
+ it("should provide a new hash if a token is added", () => {
384
+ jest
385
+ .spyOn(cryptoAssetsTokens, "listTokensForCryptoCurrency")
386
+ .mockImplementationOnce((currency) => {
387
+ const { listTokensForCryptoCurrency } = jest.requireActual(
388
+ "@ledgerhq/cryptoassets/tokens"
389
+ );
390
+ return [
391
+ ...listTokensForCryptoCurrency(currency),
392
+ {
393
+ type: "TokenCurrency",
394
+ id: "test",
395
+ ledgerSignature: "string",
396
+ contractAddress: "0x123",
397
+ parentCurrency: currency,
398
+ tokenType: "erc20",
399
+ } as TokenCurrency,
400
+ ];
401
+ });
402
+ expect(getSyncHash(currency)).not.toEqual(getSyncHash(currency));
403
+ });
404
+ });
405
+ });
406
+ });
@@ -0,0 +1,139 @@
1
+ import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
2
+ import evms from "@ledgerhq/cryptoassets/data/evm/index";
3
+ import * as CALTokensAPI from "@ledgerhq/cryptoassets/tokens";
4
+ import { ERC20Token } from "@ledgerhq/cryptoassets/types";
5
+ import network from "@ledgerhq/live-network/network";
6
+ import { fetchERC20Tokens, hydrate, preload } from "../preload";
7
+
8
+ const usdcDefinition: ERC20Token = [
9
+ "ethereum",
10
+ "usd__coin",
11
+ "USDC",
12
+ 6,
13
+ "USD Coin",
14
+ "3045022100b2e358726e4e6a6752cf344017c0e9d45b9a904120758d45f61b2804f9ad5299022015161ef28d8c4481bd9432c13562def9cce688bcfec896ef244c9a213f106cdd",
15
+ "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
16
+ false,
17
+ false,
18
+ undefined,
19
+ undefined,
20
+ ];
21
+ const usdtDefinition: ERC20Token = [
22
+ "ethereum",
23
+ "usd_tether__erc20_",
24
+ "USDT",
25
+ 6,
26
+ "Tether USD",
27
+ "3044022078c66ccea3e4dedb15a24ec3c783d7b582cd260daf62fd36afe9a8212a344aed0220160ba8c1c4b6a8aa6565bed20632a091aeeeb7bfdac67fc6589a6031acbf511c",
28
+ "0xdAC17F958D2ee523a2206206994597C13D831ec7",
29
+ false,
30
+ false,
31
+ undefined,
32
+ undefined,
33
+ ];
34
+ const currency1 = getCryptoCurrencyById("ethereum"); // chain id 1
35
+
36
+ jest.mock("@ledgerhq/live-network/network");
37
+ jest.mock("@ledgerhq/cryptoassets/data/evm/index", () => ({
38
+ get tokens() {
39
+ return {
40
+ 1: [usdcDefinition],
41
+ };
42
+ },
43
+ }));
44
+
45
+ describe("EVM Family", () => {
46
+ beforeEach(() => {
47
+ // @ts-expect-error not casted as jest mock
48
+ network.mockResolvedValue({});
49
+ });
50
+
51
+ afterEach(() => {
52
+ jest.restoreAllMocks();
53
+ });
54
+
55
+ describe("preload.ts", () => {
56
+ describe("fetchERC20Tokens", () => {
57
+ it("should load dynamically the tokens", async () => {
58
+ // @ts-expect-error not casted as jest mock
59
+ network.mockResolvedValue({ data: [usdtDefinition] });
60
+
61
+ const tokens = await fetchERC20Tokens(currency1);
62
+ expect(tokens).toEqual([usdtDefinition]);
63
+ });
64
+
65
+ it("should fallback on local CAL on dynamic CAL error", async () => {
66
+ // @ts-expect-error not casted as jest mock
67
+ network.mockImplementationOnce(async () => {
68
+ throw new Error();
69
+ });
70
+
71
+ const tokens = await fetchERC20Tokens(currency1);
72
+ expect(tokens).toEqual([usdcDefinition]);
73
+ });
74
+
75
+ it("should load erc20 tokens from local CAL when dynamic CAL undefined", async () => {
76
+ const tokens = await fetchERC20Tokens(currency1);
77
+ expect(tokens).toEqual([usdcDefinition]);
78
+ });
79
+
80
+ it("should load erc20 tokens from local CAL when dynamic CAL is empty []", async () => {
81
+ // @ts-expect-error not casted as jest mock
82
+ network.mockResolvedValue({
83
+ data: {
84
+ tokens: { 1: [] },
85
+ },
86
+ });
87
+
88
+ const tokens = await fetchERC20Tokens(currency1);
89
+ expect(tokens).toEqual([usdcDefinition]);
90
+ });
91
+
92
+ it("should return empty [] if dynamic CAL fails and local CAL fails", async () => {
93
+ jest
94
+ .spyOn(evms, "tokens", "get")
95
+ .mockImplementationOnce(() => ({} as any));
96
+
97
+ const tokens = await fetchERC20Tokens(currency1);
98
+ expect(tokens).toEqual([]);
99
+ });
100
+ });
101
+
102
+ describe("preload", () => {
103
+ it("should register tokens", async () => {
104
+ jest
105
+ .spyOn(CALTokensAPI, "addTokens")
106
+ .mockImplementationOnce(() => null);
107
+
108
+ const tokens = await preload(currency1);
109
+
110
+ expect(tokens).toEqual([usdcDefinition]);
111
+ expect(CALTokensAPI.addTokens).toHaveBeenCalledWith([
112
+ CALTokensAPI.convertERC20(usdcDefinition),
113
+ ]);
114
+ });
115
+ });
116
+
117
+ describe("hydrate", () => {
118
+ afterEach(() => {
119
+ jest.restoreAllMocks();
120
+ });
121
+
122
+ it("should return void", () => {
123
+ expect(hydrate(undefined)).toBe(undefined);
124
+ });
125
+
126
+ it("should register tokens", async () => {
127
+ jest
128
+ .spyOn(CALTokensAPI, "addTokens")
129
+ .mockImplementationOnce(() => null);
130
+
131
+ await hydrate([usdcDefinition]);
132
+
133
+ expect(CALTokensAPI.addTokens).toHaveBeenCalledWith([
134
+ CALTokensAPI.convertERC20(usdcDefinition),
135
+ ]);
136
+ });
137
+ });
138
+ });
139
+ });