@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,186 @@
1
+ import {
2
+ NotEnoughGas,
3
+ FeeNotLoaded,
4
+ InvalidAddress,
5
+ ETHAddressNonEIP,
6
+ RecipientRequired,
7
+ AmountRequired,
8
+ NotEnoughBalance,
9
+ GasLessThanEstimate,
10
+ PriorityFeeTooLow,
11
+ } from "@ledgerhq/errors";
12
+ import { ethers } from "ethers";
13
+ import BigNumber from "bignumber.js";
14
+ import { Account, AccountBridge, SubAccount } from "@ledgerhq/types-live";
15
+ import { findSubAccountById } from "@ledgerhq/coin-framework/account/index";
16
+ import {
17
+ eip1559TransactionHasFees,
18
+ getEstimatedFees,
19
+ legacyTransactionHasFees,
20
+ } from "./logic";
21
+ import {
22
+ EvmTransactionEIP1559,
23
+ EvmTransactionLegacy,
24
+ Transaction as EvmTransaction,
25
+ } from "./types";
26
+
27
+ type ValidatedTransactionFields =
28
+ | "recipient"
29
+ | "gasLimit"
30
+ | "gasPrice"
31
+ | "amount"
32
+ | "maxPriorityFee";
33
+ type ValidationIssues = Partial<Record<ValidatedTransactionFields, Error>>;
34
+
35
+ // This regex will not work with Starknet since addresses are 65 caracters long after the 0x
36
+ const ethAddressRegEx = /^(0x)?[0-9a-fA-F]{40}$/;
37
+
38
+ /**
39
+ * Validate an address for a transaction
40
+ */
41
+ export const validateRecipient = (
42
+ account: Account,
43
+ tx: EvmTransaction
44
+ ): Array<ValidationIssues> => {
45
+ const errors: ValidationIssues = {};
46
+ const warnings: ValidationIssues = {};
47
+
48
+ if (tx.recipient) {
49
+ // Check if recipient is matching the format of a valid eth address or not
50
+ const isRecipientMatchingEthFormat = tx.recipient.match(ethAddressRegEx);
51
+
52
+ if (!isRecipientMatchingEthFormat) {
53
+ errors.recipient = new InvalidAddress("", {
54
+ currency: account.currency,
55
+ });
56
+ } else {
57
+ // Check if address is respecting EIP-55
58
+ try {
59
+ const recipientChecksumed = ethers.utils.getAddress(tx.recipient);
60
+ if (tx.recipient !== recipientChecksumed) {
61
+ // this case can happen if the user is entering an ICAP address.
62
+ throw new Error();
63
+ }
64
+ } catch (e) {
65
+ // either getAddress throws for a bad checksum or we throw manually if the recipient isn't the same.
66
+ warnings.recipient = new ETHAddressNonEIP(); // "Auto-verification not available: carefully verify the address"
67
+ }
68
+ }
69
+ } else {
70
+ errors.recipient = new RecipientRequired(); // ""
71
+ }
72
+
73
+ return [errors, warnings];
74
+ };
75
+
76
+ /**
77
+ * Validate the amount of a transaction for an account
78
+ */
79
+ export const validateAmount = (
80
+ account: Account | SubAccount,
81
+ transaction: EvmTransaction,
82
+ totalSpent: BigNumber
83
+ ): Array<ValidationIssues> => {
84
+ const errors: ValidationIssues = {};
85
+ const warnings: ValidationIssues = {};
86
+
87
+ const isTokenTransaction = account?.type === "TokenAccount";
88
+ const isSmartContractInteraction = isTokenTransaction || transaction.data; // if the transaction is a smart contract interaction, it's normal that transaction has no amount
89
+ const transactionHasFees =
90
+ legacyTransactionHasFees(transaction as EvmTransactionLegacy) ||
91
+ eip1559TransactionHasFees(transaction as EvmTransactionEIP1559);
92
+ const canHaveZeroAmount = isSmartContractInteraction && transactionHasFees;
93
+
94
+ // if no amount or 0
95
+ if (
96
+ (!transaction.amount || transaction.amount.isZero()) &&
97
+ !canHaveZeroAmount
98
+ ) {
99
+ errors.amount = new AmountRequired(); // "Amount required"
100
+ } else if (totalSpent.isGreaterThan(account.balance)) {
101
+ // if not enough to make the transaction
102
+ errors.amount = new NotEnoughBalance(); // "Sorry, insufficient funds"
103
+ }
104
+ return [errors, warnings];
105
+ };
106
+
107
+ /**
108
+ * Validate gas properties of a transaction, depending on its type and the account emitter
109
+ */
110
+ export const validateGas = (
111
+ account: Account,
112
+ tx: EvmTransaction,
113
+ gasLimit: BigNumber,
114
+ estimatedFees: BigNumber
115
+ ): Array<ValidationIssues> => {
116
+ const errors: ValidationIssues = {};
117
+ const warnings: ValidationIssues = {};
118
+
119
+ if (
120
+ // if fees are not set or wrongly set
121
+ !(
122
+ legacyTransactionHasFees(tx as EvmTransactionLegacy) ||
123
+ eip1559TransactionHasFees(tx as EvmTransactionEIP1559)
124
+ )
125
+ ) {
126
+ errors.gasPrice = new FeeNotLoaded(); // "Could not load fee rates. Please set manual fees"
127
+ } else if (gasLimit.isZero()) {
128
+ errors.gasLimit = new FeeNotLoaded(); // "Could not load fee rates. Please set manual fees"
129
+ } else if (gasLimit.isLessThan(21000)) {
130
+ // minimum gas for a tx is 21000
131
+ errors.gasLimit = new GasLessThanEstimate(); // "This may be too low. Please increase"
132
+ } else if (tx.recipient && estimatedFees.gt(account.balance)) {
133
+ errors.gasPrice = new NotEnoughGas(); // "The parent account balance is insufficient for network fees"
134
+ } else if (tx.maxPriorityFeePerGas && tx.maxPriorityFeePerGas.isZero()) {
135
+ errors.maxPriorityFee = new PriorityFeeTooLow();
136
+ }
137
+
138
+ return [errors, warnings];
139
+ };
140
+
141
+ /**
142
+ * Validate a transaction and get all possibles errors and warnings about it
143
+ */
144
+ export const getTransactionStatus: AccountBridge<EvmTransaction>["getTransactionStatus"] =
145
+ async (account, tx) => {
146
+ const subAccount = findSubAccountById(account, tx.subAccountId || "");
147
+ const isTokenTransaction = subAccount?.type === "TokenAccount";
148
+ const { gasLimit, additionalFees, amount } = tx;
149
+ const estimatedFees = getEstimatedFees(tx);
150
+ const totalFees = estimatedFees.plus(additionalFees || 0);
151
+ const totalSpent = isTokenTransaction
152
+ ? tx.amount
153
+ : tx.amount.plus(totalFees);
154
+
155
+ // Recipient related errors and warnings
156
+ const [recipientErr, recipientWarn] = validateRecipient(account, tx);
157
+ // Amount related errors and warnings
158
+ const [amountErr, amountWarn] = validateAmount(
159
+ subAccount || account,
160
+ tx,
161
+ totalSpent
162
+ );
163
+ // Gas related errors and warnings
164
+ const [gasErr, gasWarn] = validateGas(account, tx, gasLimit, totalFees);
165
+
166
+ const errors: ValidationIssues = {
167
+ ...recipientErr,
168
+ ...gasErr,
169
+ ...amountErr,
170
+ };
171
+ const warnings: ValidationIssues = {
172
+ ...recipientWarn,
173
+ ...gasWarn,
174
+ ...amountWarn,
175
+ };
176
+
177
+ return {
178
+ errors,
179
+ warnings,
180
+ estimatedFees,
181
+ amount,
182
+ totalSpent,
183
+ };
184
+ };
185
+
186
+ export default getTransactionStatus;
@@ -0,0 +1,24 @@
1
+ import type { Resolver } from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
2
+ import Eth from "@ledgerhq/hw-app-eth";
3
+ import eip55 from "eip55";
4
+
5
+ /**
6
+ * Eth app binding request for the address
7
+ */
8
+ const getAddress: Resolver = async (transport, { path, verify }) => {
9
+ const ethBindings = new Eth(transport);
10
+ const { address, publicKey, chainCode } = await ethBindings.getAddress(
11
+ path,
12
+ verify,
13
+ false
14
+ );
15
+
16
+ return {
17
+ address: eip55.encode(address),
18
+ publicKey,
19
+ chainCode,
20
+ path,
21
+ };
22
+ };
23
+
24
+ export default getAddress;
package/src/logic.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { ethers } from "ethers";
2
+ import BigNumber from "bignumber.js";
3
+ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
+ import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
5
+ import { Account, SubAccount } from "@ledgerhq/types-live";
6
+ import { listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/tokens";
7
+ import { getOptimismAdditionalFees } from "./api/rpc.common";
8
+ import {
9
+ Transaction as EvmTransaction,
10
+ EvmTransactionEIP1559,
11
+ EvmTransactionLegacy,
12
+ } from "./types";
13
+
14
+ /**
15
+ * Helper to check if a legacy transaction has the right fee property
16
+ */
17
+ export const legacyTransactionHasFees = (tx: EvmTransactionLegacy): boolean =>
18
+ Boolean((!tx.type || tx.type < 2) && tx.gasPrice);
19
+
20
+ /**
21
+ * Helper to check if a legacy transaction has the right fee property
22
+ */
23
+ export const eip1559TransactionHasFees = (tx: EvmTransactionEIP1559): boolean =>
24
+ Boolean(tx.type === 2 && tx.maxFeePerGas && tx.maxPriorityFeePerGas);
25
+
26
+ /**
27
+ * Helper to get total fee value for a tx depending on its type
28
+ */
29
+ export const getEstimatedFees = (tx: EvmTransaction): BigNumber => {
30
+ if (tx.type !== 2) {
31
+ return tx.gasPrice?.multipliedBy(tx.gasLimit) || new BigNumber(0);
32
+ }
33
+ return tx.maxFeePerGas?.multipliedBy(tx.gasLimit) || new BigNumber(0);
34
+ };
35
+
36
+ /**
37
+ * Helper returning the potential additional fees necessary for layer twos
38
+ * to settle the transaction on layer 1.
39
+ */
40
+ export const getAdditionalLayer2Fees = async (
41
+ currency: CryptoCurrency,
42
+ transaction: EvmTransaction
43
+ ): Promise<BigNumber | undefined> => {
44
+ switch (currency.id) {
45
+ case "optimism":
46
+ case "optimism_goerli": {
47
+ const additionalFees = await getOptimismAdditionalFees(
48
+ currency,
49
+ transaction
50
+ );
51
+ return additionalFees;
52
+ }
53
+ default:
54
+ return;
55
+ }
56
+ };
57
+
58
+ /**
59
+ * List of properties of a sub account that can be updated when 2 "identical" accounts are found
60
+ */
61
+ const updatableSubAccountProperties: { name: string; isOps: boolean }[] = [
62
+ { name: "balance", isOps: false },
63
+ { name: "spendableBalance", isOps: false },
64
+ { name: "balanceHistoryCache", isOps: false },
65
+ { name: "swapHistory", isOps: false },
66
+ { name: "operations", isOps: true },
67
+ { name: "pendingOperations", isOps: true },
68
+ ];
69
+
70
+ /**
71
+ * In charge of smartly merging sub accounts while maintaining references as much as possible
72
+ */
73
+ export const mergeSubAccounts = (
74
+ initialAccount: Account | undefined,
75
+ newSubAccounts: Partial<SubAccount>[]
76
+ ): Array<Partial<SubAccount> | SubAccount> => {
77
+ const oldSubAccounts: Array<Partial<SubAccount> | SubAccount> | undefined =
78
+ initialAccount?.subAccounts;
79
+ if (!oldSubAccounts) {
80
+ return newSubAccounts;
81
+ }
82
+
83
+ // Creating a map of already existing sub accounts by id
84
+ const oldSubAccountsById: { [key: string]: Partial<SubAccount> } = {};
85
+ for (const oldSubAccount of oldSubAccounts) {
86
+ oldSubAccountsById[oldSubAccount.id!] = oldSubAccount;
87
+ }
88
+
89
+ // Looping on new sub accounts to compare them with already existing ones
90
+ // Already existing will be updated if necessary (see `updatableSubAccountProperties`)
91
+ // Fresh new sub accounts will be added/pushed after already existing
92
+ const newSubAccountsToAdd: Partial<SubAccount>[] = [];
93
+ for (const newSubAccount of newSubAccounts) {
94
+ const duplicatedAccount: Partial<SubAccount> | undefined =
95
+ oldSubAccountsById[newSubAccount.id!];
96
+
97
+ // If this sub account was not already in the initialAccount
98
+ if (!duplicatedAccount) {
99
+ // We'll add it later
100
+ newSubAccountsToAdd.push(newSubAccount);
101
+ continue;
102
+ }
103
+
104
+ const updates: Partial<SubAccount> = {};
105
+ for (const { name, isOps } of updatableSubAccountProperties) {
106
+ if (!isOps) {
107
+ // @ts-expect-error FIXME: fix typings
108
+ if (newSubAccount[name] !== duplicatedAccount[name]) {
109
+ // @ts-expect-error FIXME: fix typings
110
+ updates[name] = newSubAccount[name];
111
+ }
112
+ } else {
113
+ // @ts-expect-error FIXME: fix typings
114
+ updates[name] = mergeOps(duplicatedAccount[name], newSubAccount[name]);
115
+ }
116
+ }
117
+ // Updating the operationsCount in case the mergeOps changed it
118
+ updates.operationsCount =
119
+ updates.operations?.length || duplicatedAccount?.operations?.length || 0;
120
+
121
+ // Modifying the Map with the updated sub account with a new ref
122
+ oldSubAccountsById[newSubAccount.id!] = {
123
+ ...duplicatedAccount,
124
+ ...updates,
125
+ };
126
+ }
127
+ const updatedSubAccounts = Object.values(oldSubAccountsById);
128
+ return [...updatedSubAccounts, ...newSubAccountsToAdd];
129
+ };
130
+
131
+ /**
132
+ * Method creating a hash that will help triggering or not a full synchronization on an account.
133
+ * As of now, it's checking if a token has been added, removed of changed regarding important properties
134
+ */
135
+ export const getSyncHash = (currency: CryptoCurrency): string => {
136
+ const tokens = listTokensForCryptoCurrency(currency);
137
+ const basicTokensListString = tokens
138
+ .map(
139
+ (token) =>
140
+ token.id +
141
+ token.contractAddress +
142
+ token.name +
143
+ token.ticker +
144
+ token.delisted
145
+ )
146
+ .join("");
147
+
148
+ return ethers.utils.sha256(Buffer.from(basicTokensListString));
149
+ };
package/src/preload.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { log } from "@ledgerhq/logs";
2
+ import { ERC20Token } from "@ledgerhq/cryptoassets/types";
3
+ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
4
+ import { addTokens, convertERC20 } from "@ledgerhq/cryptoassets/tokens";
5
+ import { tokens as tokensByChainId } from "@ledgerhq/cryptoassets/data/evm/index";
6
+ import network from "@ledgerhq/live-network/network";
7
+ import { getEnv } from "@ledgerhq/live-env";
8
+
9
+ export const fetchERC20Tokens: (
10
+ currency: CryptoCurrency
11
+ ) => Promise<ERC20Token[]> = async (currency) => {
12
+ const { ethereumLikeInfo } = currency;
13
+
14
+ const url = `${getEnv("DYNAMIC_CAL_BASE_URL")}/evm/${
15
+ ethereumLikeInfo?.chainId || 0
16
+ }/erc20.json`;
17
+ const dynamicTokens: ERC20Token[] | null = await network({
18
+ method: "GET",
19
+ url,
20
+ })
21
+ .then(({ data }: { data: ERC20Token[] }) => (data.length ? data : null))
22
+ .catch((e) => {
23
+ log(
24
+ "error",
25
+ "EVM Family: Couldn't fetch dynamic CAL tokens from " + url,
26
+ e
27
+ );
28
+ return null;
29
+ });
30
+ if (dynamicTokens) return dynamicTokens;
31
+
32
+ // @ts-expect-error FIXME: fix typings
33
+ const tokens = tokensByChainId[ethereumLikeInfo?.chainId || ""];
34
+ if (tokens) return tokens;
35
+
36
+ log(
37
+ "warning",
38
+ `EVM Family: No tokens found in CAL for currency: ${currency.id}`,
39
+ currency
40
+ );
41
+ return [];
42
+ };
43
+
44
+ export async function preload(currency: CryptoCurrency): Promise<ERC20Token[]> {
45
+ const erc20 = await fetchERC20Tokens(currency);
46
+ addTokens(erc20.map(convertERC20));
47
+ return erc20;
48
+ }
49
+
50
+ export function hydrate(value: ERC20Token[] | null | undefined): void {
51
+ if (!Array.isArray(value)) return;
52
+ addTokens(value.map(convertERC20));
53
+ log("evm/preload", "hydrate " + value.length + " tokens");
54
+ }
@@ -0,0 +1,176 @@
1
+ import { findSubAccountById } from "@ledgerhq/coin-framework/account/index";
2
+ import { Account, TokenAccount } from "@ledgerhq/types-live";
3
+ import BigNumber from "bignumber.js";
4
+ import {
5
+ getFeesEstimation,
6
+ getGasEstimation,
7
+ getTransactionCount,
8
+ } from "./api/rpc";
9
+ import { validateRecipient } from "./getTransactionStatus";
10
+ import { getAdditionalLayer2Fees, getEstimatedFees } from "./logic";
11
+ import { getTransactionData, getTypedTransaction } from "./transaction";
12
+ import { Transaction as EvmTransaction } from "./types";
13
+
14
+ /**
15
+ * Prepare basic coin transactions or smart contract interactions (other than live ERC20 transfers)
16
+ * Should be used for transactions coming from the wallet API
17
+ * Handling addition of gas limit
18
+ */
19
+ export const prepareCoinTransaction = async (
20
+ account: Account,
21
+ typedTransaction: EvmTransaction
22
+ ): Promise<EvmTransaction> => {
23
+ // A `useAllAmount` transaction is a specific case of the live, and because we're in the
24
+ // context of a coinTransaction, no smart contract should be involed
25
+ if (typedTransaction.useAllAmount) {
26
+ // Since a gas estimation is done by simulating the transaction, we can't know in advanced how much
27
+ // we should put in the simulation.
28
+ // But as a coin transaction (no smart contract) should always consumme the same amount of gas, no matter
29
+ // the amount of coin transfered, we can infer the gasLimit with any amount.
30
+ const gasLimit = await getGasEstimation(account, {
31
+ ...typedTransaction,
32
+ amount: new BigNumber(0),
33
+ });
34
+ const draftTransaction = {
35
+ ...typedTransaction,
36
+ gasLimit,
37
+ };
38
+ const estimatedFees = getEstimatedFees(draftTransaction);
39
+ const additionalFees = await getAdditionalLayer2Fees(
40
+ account.currency,
41
+ draftTransaction
42
+ );
43
+ const amount = BigNumber.max(
44
+ account.balance.minus(estimatedFees).minus(additionalFees || 0),
45
+ 0
46
+ );
47
+
48
+ return {
49
+ ...draftTransaction,
50
+ amount,
51
+ additionalFees,
52
+ };
53
+ }
54
+
55
+ const gasLimit = await getGasEstimation(account, typedTransaction).catch(
56
+ // in case of a smart contract interaction, the gas estimation
57
+ // (which is transaction simulation by the node) can fail.
58
+ // E.g. A DApp is creating an invalid transaction, swaping more Tokens than the user actually have -> fail
59
+ // This value of 0 should be catched by `getTransactionStatus`
60
+ // and displayed in the UI as `set the gas manually`
61
+ () => new BigNumber(0)
62
+ );
63
+ const additionalFees = await getAdditionalLayer2Fees(account.currency, {
64
+ ...typedTransaction,
65
+ gasLimit,
66
+ });
67
+
68
+ return {
69
+ ...typedTransaction,
70
+ gasLimit,
71
+ additionalFees,
72
+ };
73
+ };
74
+
75
+ /**
76
+ * Prepare ERC20 transactions.
77
+ * Handling addition of ERC20 transfer data and gas limit
78
+ */
79
+ export const prepareTokenTransaction = async (
80
+ account: Account,
81
+ tokenAccount: TokenAccount,
82
+ typedTransaction: EvmTransaction
83
+ ): Promise<EvmTransaction> => {
84
+ const [recipientErrors] = validateRecipient(account, typedTransaction);
85
+ const amount = typedTransaction.useAllAmount
86
+ ? tokenAccount.balance
87
+ : typedTransaction.amount;
88
+ const data = !Object.keys(recipientErrors).length
89
+ ? getTransactionData({ ...typedTransaction, amount })
90
+ : undefined;
91
+ // As we're interacting with a smart contract,
92
+ // it's going to be the real recipient for the tx
93
+ const gasLimit = data
94
+ ? await getGasEstimation(account, {
95
+ ...typedTransaction,
96
+ amount: new BigNumber(0), // amount set to 0 as we're interacting with a smart contract
97
+ recipient: tokenAccount.token.contractAddress, // recipient is then the token smart contract
98
+ data, // buffer containing the calldata bytecode
99
+ }).catch(() => new BigNumber(0)) // this catch returning 0 should be handled by the `getTransactionStatus` method
100
+ : new BigNumber(0);
101
+ const additionalFees = await getAdditionalLayer2Fees(account.currency, {
102
+ ...typedTransaction,
103
+ amount: new BigNumber(0), // amount set to 0 as we're interacting with a smart contract
104
+ recipient: tokenAccount.token.contractAddress, // recipient is then the token smart contract
105
+ data, // buffer containing the calldata bytecode
106
+ gasLimit,
107
+ });
108
+
109
+ // Recipient isn't changed here as it would change on the UI end as well
110
+ // The change will be handled by the `prepareForSignOperation` method
111
+ return {
112
+ ...typedTransaction,
113
+ amount,
114
+ data,
115
+ gasLimit,
116
+ additionalFees,
117
+ };
118
+ };
119
+
120
+ /**
121
+ * Method called to update a transaction into a state that would make it valid
122
+ * (E.g. Adding fees, add smart contract data, etc...)
123
+ */
124
+ export const prepareTransaction = async (
125
+ account: Account,
126
+ transaction: EvmTransaction
127
+ ): Promise<EvmTransaction> => {
128
+ const { currency } = account;
129
+ // Get the current network status fees
130
+ const feeData = await getFeesEstimation(currency);
131
+ const subAccount = findSubAccountById(
132
+ account,
133
+ transaction.subAccountId || ""
134
+ );
135
+ const isTokenTransaction = subAccount?.type === "TokenAccount";
136
+ const typedTransaction = getTypedTransaction(transaction, feeData);
137
+
138
+ return isTokenTransaction
139
+ ? await prepareTokenTransaction(account, subAccount, typedTransaction)
140
+ : await prepareCoinTransaction(account, typedTransaction);
141
+ };
142
+
143
+ /**
144
+ * Prepare the transaction for the signOperation step.
145
+ * For now, used to changed the recipient for TokenAccount transfers
146
+ * with the smart contract address as recipient and add the nonce
147
+ * (which would change as well in the UI if it was done before that step)
148
+ */
149
+ export const prepareForSignOperation = async (
150
+ account: Account,
151
+ transaction: EvmTransaction
152
+ ): Promise<EvmTransaction> => {
153
+ const nonce = await getTransactionCount(
154
+ account.currency,
155
+ account.freshAddress
156
+ );
157
+
158
+ const subAccount = findSubAccountById(
159
+ account,
160
+ transaction.subAccountId || ""
161
+ );
162
+ const isTokenTransaction = subAccount?.type === "TokenAccount";
163
+
164
+ return isTokenTransaction
165
+ ? {
166
+ ...transaction,
167
+ amount: new BigNumber(0), // amount set to 0 as we're interacting with a smart contract
168
+ recipient: subAccount.token.contractAddress, // recipient is then the token smart contract
169
+ // data as already been added by the `prepareTokenTransaction` method
170
+ nonce,
171
+ }
172
+ : {
173
+ ...transaction,
174
+ nonce,
175
+ };
176
+ };