@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,317 @@
1
+ import {
2
+ decodeAccountId,
3
+ emptyHistoryCache,
4
+ encodeAccountId,
5
+ encodeTokenAccountId,
6
+ shouldRetainPendingOperation,
7
+ } from "@ledgerhq/coin-framework/account/index";
8
+ import {
9
+ AccountShapeInfo,
10
+ GetAccountShape,
11
+ makeSync,
12
+ mergeOps,
13
+ } from "@ledgerhq/coin-framework/bridge/jsHelpers";
14
+ import { log } from "@ledgerhq/logs";
15
+ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
16
+ import { Account, Operation, SubAccount } from "@ledgerhq/types-live";
17
+ import etherscanLikeApi from "./api/etherscan";
18
+ import {
19
+ getBalanceAndBlock,
20
+ getBlock,
21
+ getTokenBalance,
22
+ getTransaction,
23
+ } from "./api/rpc";
24
+ import { getSyncHash, mergeSubAccounts } from "./logic";
25
+
26
+ /**
27
+ * Switch to select one of the compatible explorer
28
+ */
29
+ const getExplorerApi = (currency: CryptoCurrency) => {
30
+ const apiType = currency.ethereumLikeInfo?.explorer?.type;
31
+
32
+ switch (apiType) {
33
+ case "etherscan":
34
+ case "blockscout":
35
+ return etherscanLikeApi;
36
+
37
+ default:
38
+ throw new Error("API type not supported");
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Main synchronization process
44
+ * Get the main Account and the potential TokenAccounts linked to it
45
+ */
46
+ export const getAccountShape: GetAccountShape = async (infos) => {
47
+ const { initialAccount, address, derivationMode, currency } = infos;
48
+ const { blockHeight, balance } = await getBalanceAndBlock(currency, address);
49
+ const accountId = encodeAccountId({
50
+ type: "js",
51
+ version: "2",
52
+ currencyId: currency.id,
53
+ xpubOrAddress: address,
54
+ derivationMode,
55
+ });
56
+ const syncHash = getSyncHash(currency);
57
+ // Due to some changes (as of now: new/updated tokens) we could need to force a sync from 0
58
+ const shouldSyncFromScratch = syncHash !== initialAccount?.syncHash;
59
+
60
+ // Get the latest stored operation to know where to start the new sync
61
+ const latestSyncedOperation = shouldSyncFromScratch
62
+ ? null
63
+ : initialAccount?.operations?.reduce<Operation | null>((acc, curr) => {
64
+ if (!acc) {
65
+ return curr;
66
+ }
67
+ return (acc?.blockHeight || 0) > (curr?.blockHeight || 0) ? acc : curr;
68
+ }, null);
69
+
70
+ // This method could not be working if the integration doesn't have an API to retreive the operations
71
+ const lastCoinOperations = await (async () => {
72
+ try {
73
+ const { getLastCoinOperations } = getExplorerApi(currency);
74
+ return await getLastCoinOperations(
75
+ currency,
76
+ address,
77
+ accountId,
78
+ latestSyncedOperation?.blockHeight || 0
79
+ );
80
+ } catch (e) {
81
+ log("EVM Family", "Failed to get latest transactions", {
82
+ address,
83
+ currency,
84
+ error: e,
85
+ });
86
+ throw e;
87
+ }
88
+ })();
89
+
90
+ const newSubAccounts = await getSubAccounts(
91
+ infos,
92
+ accountId,
93
+ shouldSyncFromScratch
94
+ );
95
+ // Merging potential new subAccouns while preserving the reference (returned value will be initialAccount.subAccounts)
96
+ const subAccounts = mergeSubAccounts(initialAccount, newSubAccounts);
97
+
98
+ // Trying to confirm pending operations that we are sure of
99
+ // because they were made in the live
100
+ // Useful for integrations without explorers
101
+ const confirmPendingOperations =
102
+ initialAccount?.pendingOperations?.map((op) =>
103
+ getOperationStatus(currency, op)
104
+ ) || [];
105
+ const confirmedOperations = await Promise.all(confirmPendingOperations).then(
106
+ (ops) => ops.filter((op): op is Operation => !!op)
107
+ );
108
+ const newOperations = [...confirmedOperations, ...lastCoinOperations];
109
+ const operations = mergeOps(initialAccount?.operations || [], newOperations);
110
+ const lastSyncDate = new Date();
111
+
112
+ return {
113
+ type: "Account",
114
+ id: accountId,
115
+ syncHash,
116
+ balance,
117
+ spendableBalance: balance,
118
+ blockHeight,
119
+ operations,
120
+ operationsCount: operations.length,
121
+ subAccounts,
122
+ lastSyncDate,
123
+ } as Partial<Account>;
124
+ };
125
+
126
+ /**
127
+ * Getting all token related operations in order to provide TokenAccounts
128
+ */
129
+ export const getSubAccounts = async (
130
+ infos: AccountShapeInfo,
131
+ accountId: string,
132
+ shouldSyncFromScratch = false
133
+ ): Promise<Partial<SubAccount>[]> => {
134
+ const { initialAccount, address, currency } = infos;
135
+
136
+ // Get the latest operation from all subaccounts
137
+ const latestSyncedOperation = shouldSyncFromScratch
138
+ ? null
139
+ : initialAccount?.subAccounts
140
+ ?.flatMap(({ operations }) => operations)
141
+ .reduce<Operation | null>((acc, curr) => {
142
+ if (!acc) {
143
+ return curr;
144
+ }
145
+ return (acc?.blockHeight || 0) > (curr?.blockHeight || 0)
146
+ ? acc
147
+ : curr;
148
+ }, null);
149
+
150
+ // This method could not be working if the integration doesn't have an API to retreive the operations
151
+ const lastERC20OperationsAndCurrencies = await (async () => {
152
+ try {
153
+ const { getLastTokenOperations } = getExplorerApi(currency);
154
+ return await getLastTokenOperations(
155
+ currency,
156
+ address,
157
+ accountId,
158
+ latestSyncedOperation?.blockHeight || 0
159
+ );
160
+ } catch (e) {
161
+ log("EVM Family", "Failed to get latest ERC20 transactions", {
162
+ address,
163
+ currency,
164
+ error: e,
165
+ });
166
+ throw e;
167
+ }
168
+ })();
169
+
170
+ // Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
171
+ const erc20OperationsByToken = lastERC20OperationsAndCurrencies.reduce<
172
+ Map<TokenCurrency, Operation[]>
173
+ >((acc, { tokenCurrency, operation }) => {
174
+ if (!tokenCurrency) return acc;
175
+
176
+ if (!acc.has(tokenCurrency)) {
177
+ acc.set(tokenCurrency, []);
178
+ }
179
+ acc.get(tokenCurrency)?.push(operation);
180
+
181
+ return acc;
182
+ }, new Map<TokenCurrency, Operation[]>());
183
+
184
+ // Fetching all TokenAccounts possible and providing already filtered operations
185
+ const subAccountsPromises: Promise<Partial<SubAccount>>[] = [];
186
+ for (const [token, ops] of erc20OperationsByToken.entries()) {
187
+ subAccountsPromises.push(
188
+ getSubAccountShape(currency, accountId, token, ops)
189
+ );
190
+ }
191
+
192
+ return Promise.all(subAccountsPromises);
193
+ };
194
+
195
+ /**
196
+ * Fetch the balance for a token and creates a TokenAccount based on this and the provided operations
197
+ */
198
+ export const getSubAccountShape = async (
199
+ currency: CryptoCurrency,
200
+ parentId: string,
201
+ token: TokenCurrency,
202
+ operations: Operation[]
203
+ ): Promise<Partial<SubAccount>> => {
204
+ const { xpubOrAddress: address } = decodeAccountId(parentId);
205
+ const tokenAccountId = encodeTokenAccountId(parentId, token);
206
+ const balance = await getTokenBalance(
207
+ currency,
208
+ address,
209
+ token.contractAddress
210
+ );
211
+
212
+ return {
213
+ type: "TokenAccount",
214
+ id: tokenAccountId,
215
+ parentId,
216
+ token,
217
+ balance,
218
+ spendableBalance: balance,
219
+ creationDate: new Date(),
220
+ operations,
221
+ operationsCount: operations.length,
222
+ pendingOperations: [],
223
+ balanceHistoryCache: emptyHistoryCache,
224
+ swapHistory: [],
225
+ };
226
+ };
227
+
228
+ /**
229
+ * Get a finalized operation depending on it status (confirmed or not)
230
+ */
231
+ export const getOperationStatus = async (
232
+ currency: CryptoCurrency,
233
+ op: Operation
234
+ ): Promise<Operation | null> => {
235
+ try {
236
+ const {
237
+ blockNumber: blockHeight,
238
+ blockHash,
239
+ timestamp,
240
+ nonce,
241
+ } = await getTransaction(currency, op.hash);
242
+
243
+ if (!blockHeight) {
244
+ throw new Error("getOperationStatus: Transaction has no block");
245
+ }
246
+
247
+ const date = await (async () => {
248
+ // timestamp can be missing depending on the node
249
+ if (timestamp) {
250
+ return new Date(timestamp * 1000);
251
+ }
252
+
253
+ // Without timestamp, we directly look for the block
254
+ const { timestamp: blockTimestamp } = await getBlock(
255
+ currency,
256
+ blockHeight
257
+ );
258
+ return new Date(blockTimestamp * 1000);
259
+ })();
260
+
261
+ return {
262
+ ...op,
263
+ transactionSequenceNumber: nonce,
264
+ blockHash,
265
+ blockHeight,
266
+ date,
267
+ } as Operation;
268
+ } catch (e) {
269
+ return null;
270
+ }
271
+ };
272
+
273
+ /**
274
+ * After each sync, it might be necessary to remove pending operations
275
+ * inside of subAccounts.
276
+ */
277
+ export const postSync = (initial: Account, synced: Account): Account => {
278
+ // Set of hashes from the pending operations of the main account
279
+ const coinPendingOperationsHashes = new Set();
280
+ for (const coinPendingOperation of synced.pendingOperations) {
281
+ coinPendingOperationsHashes.add(coinPendingOperation.hash);
282
+ }
283
+ // Set of ids from the already existing subAccount from previous sync
284
+ const initialSubAccountsIds = new Set();
285
+ for (const subAccount of initial.subAccounts || []) {
286
+ initialSubAccountsIds.add(subAccount.id);
287
+ }
288
+
289
+ return {
290
+ ...synced,
291
+ subAccounts: synced.subAccounts?.map((subAccount) => {
292
+ // If the subAccount is new, just return the freshly synced subAccount
293
+ if (!initialSubAccountsIds.has(subAccount.id)) return subAccount;
294
+
295
+ return {
296
+ ...subAccount,
297
+ pendingOperations: subAccount.pendingOperations.filter(
298
+ (tokenPendingOperation) =>
299
+ // if the pending operation got removed from the main account, remove it as well
300
+ coinPendingOperationsHashes.has(tokenPendingOperation.hash) &&
301
+ // if the transaction has been confirmed, remove it
302
+ !subAccount.operations.some(
303
+ (op) => op.hash === tokenPendingOperation.hash
304
+ ) &&
305
+ // common rule for pending operations retention in the live
306
+ shouldRetainPendingOperation(synced, tokenPendingOperation)
307
+ ),
308
+ };
309
+ }),
310
+ };
311
+ };
312
+
313
+ export const sync = makeSync({
314
+ getAccountShape,
315
+ postSync,
316
+ shouldMergeOps: false,
317
+ });
@@ -0,0 +1,153 @@
1
+ import {
2
+ decodeAccountId,
3
+ decodeTokenAccountId,
4
+ encodeTokenAccountId,
5
+ shortAddressPreview,
6
+ } from "@ledgerhq/coin-framework/account/index";
7
+ import {
8
+ DerivationMode,
9
+ getDerivationScheme,
10
+ runDerivationScheme,
11
+ } from "@ledgerhq/coin-framework/derivation";
12
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
13
+ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
14
+ import {
15
+ Account,
16
+ Operation,
17
+ SubAccount,
18
+ TokenAccount,
19
+ } from "@ledgerhq/types-live";
20
+ import BigNumber from "bignumber.js";
21
+
22
+ export const makeAccount = (
23
+ address: string,
24
+ currency: CryptoCurrency,
25
+ subAccounts: SubAccount[] = []
26
+ ): Account => {
27
+ const id = `js:2:${currency.id}:${address}:`;
28
+ const { derivationMode, xpubOrAddress } = decodeAccountId(id);
29
+ const scheme = getDerivationScheme({
30
+ derivationMode: derivationMode as DerivationMode,
31
+ currency,
32
+ });
33
+ const index = 0;
34
+ const freshAddressPath = runDerivationScheme(scheme, currency, {
35
+ account: index,
36
+ node: 0,
37
+ address: 0,
38
+ });
39
+
40
+ const account: Account = {
41
+ type: "Account",
42
+ name:
43
+ currency.name +
44
+ " " +
45
+ (derivationMode || "legacy") +
46
+ " " +
47
+ shortAddressPreview(xpubOrAddress),
48
+ xpub: xpubOrAddress,
49
+ subAccounts,
50
+ seedIdentifier: xpubOrAddress,
51
+ starred: true,
52
+ used: true,
53
+ swapHistory: [],
54
+ id,
55
+ derivationMode,
56
+ currency,
57
+ unit: currency.units[0],
58
+ index,
59
+ freshAddress: xpubOrAddress,
60
+ freshAddressPath,
61
+ freshAddresses: [],
62
+ creationDate: new Date(),
63
+ lastSyncDate: new Date(0),
64
+ blockHeight: 0,
65
+ balance: new BigNumber(0),
66
+ spendableBalance: new BigNumber(0),
67
+ operationsCount: 0,
68
+ operations: [],
69
+ pendingOperations: [],
70
+ balanceHistoryCache: {
71
+ HOUR: {
72
+ latestDate: null,
73
+ balances: [],
74
+ },
75
+ DAY: {
76
+ latestDate: null,
77
+ balances: [],
78
+ },
79
+ WEEK: {
80
+ latestDate: null,
81
+ balances: [],
82
+ },
83
+ },
84
+ };
85
+
86
+ return account;
87
+ };
88
+
89
+ export const makeTokenAccount = (
90
+ address: string,
91
+ tokenCurrency: TokenCurrency
92
+ ): TokenAccount => {
93
+ const { parentCurrency: currency } = tokenCurrency;
94
+ const account = makeAccount(address, currency);
95
+
96
+ const tokenAccountId = encodeTokenAccountId(account.id, tokenCurrency);
97
+
98
+ return {
99
+ type: "TokenAccount",
100
+ id: tokenAccountId,
101
+ parentId: account.id,
102
+ token: tokenCurrency,
103
+ balance: new BigNumber(0),
104
+ spendableBalance: new BigNumber(0),
105
+ creationDate: new Date(),
106
+ operationsCount: 0,
107
+ operations: [],
108
+ pendingOperations: [],
109
+ starred: false,
110
+ balanceHistoryCache: {
111
+ HOUR: {
112
+ latestDate: null,
113
+ balances: [],
114
+ },
115
+ DAY: {
116
+ latestDate: null,
117
+ balances: [],
118
+ },
119
+ WEEK: {
120
+ latestDate: null,
121
+ balances: [],
122
+ },
123
+ },
124
+ swapHistory: [],
125
+ };
126
+ };
127
+
128
+ export const makeOperation = (partialOp?: Partial<Operation>): Operation => {
129
+ const accountId = partialOp?.accountId ?? "js:2:ethereum:0xkvn:";
130
+ const { xpubOrAddress } = decodeAccountId(
131
+ accountId.includes("+")
132
+ ? decodeTokenAccountId(accountId).accountId
133
+ : accountId
134
+ );
135
+ const hash = partialOp?.hash ?? "0xhash";
136
+ const type = partialOp?.type ?? "OUT";
137
+ return {
138
+ id: encodeOperationId(accountId, hash, type),
139
+ hash,
140
+ type,
141
+ value: new BigNumber(0),
142
+ fee: new BigNumber(0),
143
+ blockHash: null,
144
+ blockHeight: null,
145
+ senders: [xpubOrAddress],
146
+ recipients: ["0xlmb"],
147
+ accountId,
148
+ transactionSequenceNumber: 0,
149
+ date: new Date(),
150
+ extra: {},
151
+ ...partialOp,
152
+ };
153
+ };
@@ -0,0 +1,193 @@
1
+ import { getAccountUnit } from "@ledgerhq/coin-framework/account/index";
2
+ import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
3
+ import {
4
+ formatTransactionStatusCommon as formatTransactionStatus,
5
+ fromTransactionCommonRaw,
6
+ fromTransactionStatusRawCommon as fromTransactionStatusRaw,
7
+ toTransactionCommonRaw,
8
+ toTransactionStatusRawCommon as toTransactionStatusRaw,
9
+ } from "@ledgerhq/coin-framework/transaction/common";
10
+ import type { Account } from "@ledgerhq/types-live";
11
+ import { BigNumber } from "bignumber.js";
12
+ import { ethers } from "ethers";
13
+ import ERC20ABI from "./abis/erc20.abi.json";
14
+ import { transactionToEthersTransaction } from "./adapters";
15
+ import type {
16
+ Transaction as EvmTransaction,
17
+ EvmTransactionEIP1559,
18
+ EvmTransactionLegacy,
19
+ TransactionRaw as EvmTransactionRaw,
20
+ FeeData,
21
+ } from "./types";
22
+
23
+ export const DEFAULT_GAS_LIMIT = new BigNumber(21000);
24
+
25
+ /**
26
+ * Format the transaction for the CLI
27
+ */
28
+ export const formatTransaction = (
29
+ { mode, amount, recipient, useAllAmount }: EvmTransaction,
30
+ account: Account
31
+ ): string =>
32
+ `
33
+ ${mode.toUpperCase()} ${
34
+ useAllAmount
35
+ ? "MAX"
36
+ : amount.isZero()
37
+ ? ""
38
+ : " " +
39
+ formatCurrencyUnit(getAccountUnit(account), amount, {
40
+ showCode: true,
41
+ disableRounding: true,
42
+ })
43
+ }${recipient ? `\nTO ${recipient}` : ""}`;
44
+
45
+ /**
46
+ * Serializer raw to transaction
47
+ */
48
+ export const fromTransactionRaw = (
49
+ rawTx: EvmTransactionRaw
50
+ ): EvmTransaction => {
51
+ const common = fromTransactionCommonRaw(rawTx);
52
+ const tx: Partial<EvmTransaction> = {
53
+ ...common,
54
+ family: rawTx.family,
55
+ mode: rawTx.mode,
56
+ chainId: rawTx.chainId,
57
+ nonce: rawTx.nonce,
58
+ gasLimit: new BigNumber(rawTx.gasLimit),
59
+ feesStrategy: rawTx.feesStrategy,
60
+ type: rawTx.type ?? 0, // if rawTx.type is undefined, transaction will be considered legacy and therefore type 0
61
+ };
62
+
63
+ if (rawTx.data) {
64
+ tx.data = Buffer.from(rawTx.data, "hex");
65
+ }
66
+
67
+ if (rawTx.gasPrice) {
68
+ tx.gasPrice = new BigNumber(rawTx.gasPrice);
69
+ }
70
+
71
+ if (rawTx.maxFeePerGas) {
72
+ tx.maxFeePerGas = new BigNumber(rawTx.maxFeePerGas);
73
+ }
74
+
75
+ if (rawTx.maxPriorityFeePerGas) {
76
+ tx.maxPriorityFeePerGas = new BigNumber(rawTx.maxPriorityFeePerGas);
77
+ }
78
+
79
+ if (rawTx.additionalFees) {
80
+ tx.additionalFees = new BigNumber(rawTx.additionalFees);
81
+ }
82
+
83
+ return tx as EvmTransaction;
84
+ };
85
+
86
+ /**
87
+ * Serializer transaction to raw
88
+ */
89
+ export const toTransactionRaw = (tx: EvmTransaction): EvmTransactionRaw => {
90
+ const common = toTransactionCommonRaw(tx);
91
+ const txRaw: Partial<EvmTransactionRaw> = {
92
+ ...common,
93
+ family: tx.family,
94
+ mode: tx.mode,
95
+ chainId: tx.chainId,
96
+ nonce: tx.nonce,
97
+ gasLimit: tx.gasLimit.toFixed(),
98
+ feesStrategy: tx.feesStrategy,
99
+ type: tx.type,
100
+ };
101
+
102
+ if (tx.data) {
103
+ txRaw.data = Buffer.from(tx.data).toString("hex");
104
+ }
105
+
106
+ if (tx.gasPrice) {
107
+ txRaw.gasPrice = tx.gasPrice.toFixed();
108
+ }
109
+
110
+ if (tx.maxFeePerGas) {
111
+ txRaw.maxFeePerGas = tx.maxFeePerGas.toFixed();
112
+ }
113
+
114
+ if (tx.maxPriorityFeePerGas) {
115
+ txRaw.maxPriorityFeePerGas = tx.maxPriorityFeePerGas.toFixed();
116
+ }
117
+
118
+ if (tx.additionalFees) {
119
+ txRaw.additionalFees = tx.additionalFees.toFixed();
120
+ }
121
+
122
+ return txRaw as EvmTransactionRaw;
123
+ };
124
+
125
+ /**
126
+ * Returns the data necessary to execute smart contracts.
127
+ * As of now, only used to create ERC20 transfers' data
128
+ */
129
+ export const getTransactionData = (
130
+ transaction: EvmTransaction
131
+ ): Buffer | undefined => {
132
+ const contract = new ethers.utils.Interface(ERC20ABI);
133
+ const data = contract.encodeFunctionData("transfer", [
134
+ transaction.recipient,
135
+ transaction.amount.toFixed(),
136
+ ]);
137
+
138
+ return data ? Buffer.from(data.slice(2), "hex") : undefined;
139
+ };
140
+
141
+ /**
142
+ * Returns a transaction with the correct type and entries depending
143
+ * on the network compatiblity.
144
+ */
145
+ export const getTypedTransaction = (
146
+ transaction: EvmTransaction,
147
+ feeData: FeeData
148
+ ): EvmTransaction => {
149
+ // If the blockchain is supporting EIP-1559, use maxFeePerGas & maxPriorityFeePerGas
150
+ if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
151
+ delete transaction.gasPrice;
152
+ return {
153
+ ...transaction,
154
+ maxFeePerGas: feeData.maxFeePerGas,
155
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
156
+ type: 2,
157
+ } as EvmTransactionEIP1559;
158
+ }
159
+
160
+ // Else just use a legacy transaction
161
+ delete transaction.maxFeePerGas;
162
+ delete transaction.maxPriorityFeePerGas;
163
+ return {
164
+ ...transaction,
165
+ gasPrice: feeData.gasPrice || new BigNumber(0),
166
+ type: 0,
167
+ } as EvmTransactionLegacy;
168
+ };
169
+
170
+ /**
171
+ * Serialize a Ledger Live transaction into an hex string
172
+ */
173
+ export const getSerializedTransaction = (
174
+ tx: EvmTransaction,
175
+ signature?: Partial<ethers.Signature>
176
+ ): string => {
177
+ const unsignedEthersTransaction = transactionToEthersTransaction(tx);
178
+
179
+ return ethers.utils.serializeTransaction(
180
+ unsignedEthersTransaction,
181
+ signature as ethers.Signature
182
+ );
183
+ };
184
+
185
+ export default {
186
+ formatTransaction,
187
+ fromTransactionRaw,
188
+ toTransactionRaw,
189
+ toTransactionStatusRaw,
190
+ formatTransactionStatus,
191
+ fromTransactionStatusRaw,
192
+ getSerializedTransaction,
193
+ };