@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,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
|
+
};
|