@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,148 @@
1
+ import eip55 from "eip55";
2
+ import { ethers } from "ethers";
3
+ import BigNumber from "bignumber.js";
4
+ import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
5
+ import { Operation, OperationType } from "@ledgerhq/types-live";
6
+ import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets";
7
+ import {
8
+ decodeAccountId,
9
+ encodeTokenAccountId,
10
+ } from "@ledgerhq/coin-framework/account/index";
11
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
12
+ import {
13
+ Transaction as EvmTransaction,
14
+ EvmTransactionEIP1559,
15
+ EvmTransactionLegacy,
16
+ EtherscanOperation,
17
+ EtherscanERC20Event,
18
+ } from "./types";
19
+
20
+ /**
21
+ * Adapter to convert a Ledger Live transaction to an Ethers transaction
22
+ */
23
+ export const transactionToEthersTransaction = (
24
+ tx: EvmTransaction
25
+ ): ethers.Transaction => {
26
+ const ethersTx = {
27
+ to: tx.recipient,
28
+ value: ethers.BigNumber.from(tx.amount.toFixed()),
29
+ data: tx.data ? `0x${tx.data.toString("hex")}` : undefined,
30
+ gasLimit: ethers.BigNumber.from(tx.gasLimit.toFixed()),
31
+ nonce: tx.nonce,
32
+ chainId: tx.chainId,
33
+ type: tx.type,
34
+ } as Partial<ethers.Transaction>;
35
+
36
+ // is EIP-1559 transaction (type 2)
37
+ if (tx.type === 2) {
38
+ ethersTx.maxFeePerGas = ethers.BigNumber.from(
39
+ (tx as EvmTransactionEIP1559).maxFeePerGas.toFixed()
40
+ );
41
+ ethersTx.maxPriorityFeePerGas = ethers.BigNumber.from(
42
+ (tx as EvmTransactionEIP1559).maxPriorityFeePerGas.toFixed()
43
+ );
44
+ } else {
45
+ // is Legacy transaction (type 0)
46
+ ethersTx.gasPrice = ethers.BigNumber.from(
47
+ (tx as EvmTransactionLegacy).gasPrice.toFixed()
48
+ );
49
+ }
50
+
51
+ return ethersTx as ethers.Transaction;
52
+ };
53
+
54
+ /**
55
+ * Adapter to convert an Etherscan-like operation into a Ledger Live Operation
56
+ */
57
+ export const etherscanOperationToOperation = (
58
+ accountId: string,
59
+ tx: EtherscanOperation
60
+ ): Operation => {
61
+ const { xpubOrAddress: address } = decodeAccountId(accountId);
62
+ const checksummedAddress = eip55.encode(address);
63
+ const from = eip55.encode(tx.from);
64
+ const to = tx.to ? eip55.encode(tx.to) : "";
65
+ const value = new BigNumber(tx.value);
66
+ const fee = new BigNumber(tx.gasUsed).times(new BigNumber(tx.gasPrice));
67
+
68
+ const type = ((): OperationType => {
69
+ if (to === checksummedAddress) {
70
+ return "IN";
71
+ }
72
+ if (from === checksummedAddress) {
73
+ return value.eq(0) ? "FEES" : "OUT";
74
+ }
75
+
76
+ return "NONE";
77
+ })();
78
+
79
+ return {
80
+ id: encodeOperationId(accountId, tx.hash, type),
81
+ hash: tx.hash,
82
+ type: type,
83
+ value: type === "OUT" || type === "FEES" ? value.plus(fee) : value,
84
+ fee,
85
+ senders: [from],
86
+ recipients: [to],
87
+ blockHeight: parseInt(tx.blockNumber, 10),
88
+ blockHash: tx.blockHash,
89
+ transactionSequenceNumber: parseInt(tx.nonce, 10),
90
+ accountId: accountId,
91
+ date: new Date(parseInt(tx.timeStamp, 10) * 1000),
92
+ extra: {},
93
+ };
94
+ };
95
+
96
+ /**
97
+ * Adapter to convert an ERC20 transaction received
98
+ * on etherscan-like APIs into an Operation
99
+ */
100
+ export const etherscanERC20EventToOperation = (
101
+ accountId: string,
102
+ event: EtherscanERC20Event
103
+ ): { tokenCurrency: TokenCurrency; operation: Operation } | null => {
104
+ const { currencyId, xpubOrAddress: address } = decodeAccountId(accountId);
105
+ const tokenCurrency = findTokenByAddressInCurrency(
106
+ event.contractAddress,
107
+ currencyId
108
+ );
109
+ if (!tokenCurrency) return null;
110
+
111
+ const tokenAccountId = encodeTokenAccountId(accountId, tokenCurrency);
112
+ const from = eip55.encode(event.from);
113
+ const to = event.to ? eip55.encode(event.to) : "";
114
+ const value = new BigNumber(event.value);
115
+ const fee = new BigNumber(event.gasUsed).times(new BigNumber(event.gasPrice));
116
+
117
+ const type = ((): OperationType => {
118
+ if (event.contractAddress && to === eip55.encode(address)) {
119
+ return "IN";
120
+ }
121
+
122
+ if (event.contractAddress && from === eip55.encode(address)) {
123
+ return "OUT";
124
+ }
125
+
126
+ return "NONE";
127
+ })();
128
+
129
+ return {
130
+ tokenCurrency,
131
+ operation: {
132
+ id: encodeOperationId(tokenAccountId, event.hash, type),
133
+ hash: event.hash,
134
+ type: type,
135
+ value,
136
+ fee,
137
+ senders: [from],
138
+ recipients: [to],
139
+ contract: tokenCurrency.contractAddress,
140
+ blockHeight: parseInt(event.blockNumber, 10),
141
+ blockHash: event.blockHash,
142
+ transactionSequenceNumber: parseInt(event.nonce, 10),
143
+ accountId: tokenAccountId,
144
+ date: new Date(parseInt(event.timeStamp, 10) * 1000),
145
+ extra: {},
146
+ },
147
+ };
148
+ };
@@ -0,0 +1,124 @@
1
+ import { delay } from "@ledgerhq/live-promise";
2
+ import { Operation } from "@ledgerhq/types-live";
3
+ import axios, { AxiosRequestConfig } from "axios";
4
+ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
5
+ import { makeLRUCache } from "@ledgerhq/live-network/cache";
6
+ import { EtherscanERC20Event, EtherscanOperation } from "../types";
7
+ import { EtherscanAPIError } from "../errors";
8
+ import {
9
+ etherscanERC20EventToOperation,
10
+ etherscanOperationToOperation,
11
+ } from "../adapters";
12
+
13
+ export const ETHERSCAN_TIMEOUT = 5000; // 5 seconds between 2 calls
14
+ export const DEFAULT_RETRIES_API = 8;
15
+
16
+ async function fetchWithRetries<T>(
17
+ params: AxiosRequestConfig,
18
+ retries = DEFAULT_RETRIES_API
19
+ ): Promise<T> {
20
+ try {
21
+ const { data } = await axios.request<{
22
+ status: string;
23
+ message: string;
24
+ result: T;
25
+ }>(params);
26
+
27
+ if (!Number(data.status) && data.message === "NOTOK") {
28
+ throw new EtherscanAPIError(
29
+ "Error while fetching data from Etherscan like API",
30
+ { params, data }
31
+ );
32
+ }
33
+
34
+ return data.result;
35
+ } catch (e) {
36
+ if (retries) {
37
+ // wait the API timeout before trying again
38
+ await delay(ETHERSCAN_TIMEOUT);
39
+ // decrement with prefix here or it won't work
40
+ return fetchWithRetries<T>(params, --retries);
41
+ }
42
+ throw e;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get all the last "normal" transactions (no tokens / NFTs)
48
+ */
49
+ export const getLastCoinOperations = makeLRUCache<
50
+ [
51
+ currency: CryptoCurrency,
52
+ address: string,
53
+ accountId: string,
54
+ fromBlock: number
55
+ ],
56
+ Operation[]
57
+ >(
58
+ async (currency, address, accountId, fromBlock) => {
59
+ const apiDomain = currency.ethereumLikeInfo?.explorer?.uri;
60
+ if (!apiDomain) {
61
+ return [];
62
+ }
63
+
64
+ let url = `${apiDomain}/api?module=account&action=txlist&address=${address}&tag=latest&page=1&sort=desc`;
65
+ if (fromBlock) {
66
+ url += `&startBlock=${fromBlock}`;
67
+ }
68
+
69
+ const ops = await fetchWithRetries<EtherscanOperation[]>({
70
+ method: "GET",
71
+ url,
72
+ });
73
+
74
+ return ops
75
+ .map((tx) => etherscanOperationToOperation(accountId, tx))
76
+ .filter(Boolean) as Operation[];
77
+ },
78
+ (currency, address, accountId, fromBlock) => accountId + fromBlock,
79
+ { ttl: 5 * 1000 }
80
+ );
81
+
82
+ /**
83
+ * Get all the last ERC20 transactions
84
+ */
85
+ export const getLastTokenOperations = makeLRUCache<
86
+ [
87
+ currency: CryptoCurrency,
88
+ address: string,
89
+ accountId: string,
90
+ fromBlock: number
91
+ ],
92
+ { tokenCurrency: TokenCurrency; operation: Operation }[]
93
+ >(
94
+ async (currency, address, accountId, fromBlock) => {
95
+ const apiDomain = currency.ethereumLikeInfo?.explorer?.uri;
96
+ if (!apiDomain) {
97
+ return [];
98
+ }
99
+
100
+ let url = `${apiDomain}/api?module=account&action=tokentx&address=${address}&tag=latest&page=1&sort=desc`;
101
+ if (fromBlock) {
102
+ url += `&startBlock=${fromBlock}`;
103
+ }
104
+
105
+ const ops = await fetchWithRetries<EtherscanERC20Event[]>({
106
+ method: "GET",
107
+ url,
108
+ });
109
+
110
+ return ops
111
+ .map((event) => etherscanERC20EventToOperation(accountId, event))
112
+ .filter(Boolean) as {
113
+ tokenCurrency: TokenCurrency;
114
+ operation: Operation;
115
+ }[];
116
+ },
117
+ (currency, address, accountId, fromBlock) => accountId + fromBlock,
118
+ { ttl: 5 * 1000 }
119
+ );
120
+
121
+ export default {
122
+ getLastCoinOperations,
123
+ getLastTokenOperations,
124
+ };
@@ -0,0 +1,354 @@
1
+ /** ⚠️ keep this order of import. @see https://docs.ethers.io/v5/cookbook/react-native/#cookbook-reactnative ⚠️ */
2
+ import { ethers } from "ethers";
3
+ import BigNumber from "bignumber.js";
4
+ import { log } from "@ledgerhq/logs";
5
+ import { delay } from "@ledgerhq/live-promise";
6
+ import { Account } from "@ledgerhq/types-live";
7
+ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
8
+ import { makeLRUCache } from "@ledgerhq/live-network/cache";
9
+ import OptimismGasPriceOracleAbi from "../abis/optimismGasPriceOracle.abi.json";
10
+ import { FeeData, FeeHistory, Transaction as EvmTransaction } from "../types";
11
+ import { GasEstimationError, InsufficientFunds } from "../errors";
12
+ import { transactionToEthersTransaction } from "../adapters";
13
+ import { getSerializedTransaction } from "../transaction";
14
+ import ERC20Abi from "../abis/erc20.abi.json";
15
+
16
+ export const RPC_TIMEOUT = 5000; // wait 5 sec after a fail
17
+ export const DEFAULT_RETRIES_RPC_METHODS = 3;
18
+
19
+ /**
20
+ * Cache for RPC providers to avoid recreating the connection on every usage of `withApi`
21
+ * Without this, ethers will create a new provider and use the `eth_chainId` RPC call
22
+ * at instanciation which could result in rate limits being reached
23
+ * on some specific nodes (E.g. the main Optimism RPC)
24
+ */
25
+ const PROVIDERS_BY_RPC: Record<string, ethers.providers.StaticJsonRpcProvider> =
26
+ {};
27
+
28
+ /**
29
+ * Connects to RPC Node
30
+ *
31
+ * ⚠️ Make sure to always use a `StaticJsonRpcProvider` and not a `JsonRpcProvider`
32
+ * because the latter will use a `eth_chainId` before every request in order
33
+ * to check if the node used changed (as per EIP-1193 standard)
34
+ * @see https://github.com/ethers-io/ethers.js/issues/901
35
+ */
36
+ export async function withApi<T>(
37
+ currency: CryptoCurrency,
38
+ execute: (api: ethers.providers.StaticJsonRpcProvider) => Promise<T>,
39
+ retries = DEFAULT_RETRIES_RPC_METHODS
40
+ ): Promise<T> {
41
+ if (!currency?.ethereumLikeInfo?.rpc) {
42
+ throw new Error("Currency doesn't have an RPC node provided");
43
+ }
44
+
45
+ try {
46
+ if (!PROVIDERS_BY_RPC[currency.ethereumLikeInfo.rpc]) {
47
+ PROVIDERS_BY_RPC[currency.ethereumLikeInfo.rpc] =
48
+ new ethers.providers.StaticJsonRpcProvider(
49
+ currency.ethereumLikeInfo.rpc
50
+ );
51
+ }
52
+
53
+ const provider = PROVIDERS_BY_RPC[currency.ethereumLikeInfo.rpc];
54
+ return await execute(provider);
55
+ } catch (e) {
56
+ if (retries) {
57
+ // wait the RPC timeout before trying again
58
+ await delay(RPC_TIMEOUT);
59
+ // decrement with prefix here or it won't work
60
+ return withApi<T>(currency, execute, --retries);
61
+ }
62
+ throw e;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get account balance and last chain block
68
+ */
69
+ export const getBalanceAndBlock: (
70
+ currency: CryptoCurrency,
71
+ addr: string
72
+ ) => Promise<{ blockHeight: number; balance: BigNumber }> = async (
73
+ currency,
74
+ addr
75
+ ) =>
76
+ withApi(currency, async (api) => {
77
+ const [balance, blockHeight] = await Promise.all([
78
+ getCoinBalance(currency, addr),
79
+ api.getBlockNumber(),
80
+ ]);
81
+
82
+ return {
83
+ blockHeight,
84
+ balance: new BigNumber(balance.toString()),
85
+ };
86
+ });
87
+
88
+ /**
89
+ * Get a transaction by hash
90
+ */
91
+ export const getTransaction = (
92
+ currency: CryptoCurrency,
93
+ hash: string
94
+ ): Promise<ethers.providers.TransactionResponse> =>
95
+ withApi(currency, (api) => {
96
+ return api.getTransaction(hash);
97
+ });
98
+
99
+ /**
100
+ * Get the balance of an address
101
+ */
102
+ export const getCoinBalance = (
103
+ currency: CryptoCurrency,
104
+ address: string
105
+ ): Promise<BigNumber> =>
106
+ withApi(currency, async (api) => {
107
+ const balance = await api.getBalance(address);
108
+ return new BigNumber(balance.toString());
109
+ });
110
+
111
+ /**
112
+ * Get the balance of an address
113
+ */
114
+ export const getTokenBalance = (
115
+ currency: CryptoCurrency,
116
+ address: string,
117
+ contractAddress: string
118
+ ): Promise<BigNumber> =>
119
+ withApi(currency, async (api) => {
120
+ const erc20 = new ethers.Contract(contractAddress, ERC20Abi, api);
121
+ const balance = await erc20.balanceOf(address);
122
+ return new BigNumber(balance.toString());
123
+ });
124
+
125
+ /**
126
+ * Get account nonce
127
+ */
128
+ export const getTransactionCount = (
129
+ currency: CryptoCurrency,
130
+ addr: string
131
+ ): Promise<number> =>
132
+ withApi(currency, async (api) => {
133
+ return api.getTransactionCount(addr);
134
+ });
135
+
136
+ /**
137
+ * Get an estimated gas limit for a transaction
138
+ */
139
+ export const getGasEstimation = (
140
+ account: Account,
141
+ transaction: EvmTransaction
142
+ ): Promise<BigNumber> =>
143
+ withApi(account.currency, async (api) => {
144
+ const { to, value, data } = transactionToEthersTransaction(transaction);
145
+
146
+ try {
147
+ const gasEtimation = await api.estimateGas({
148
+ from: account.freshAddress, // should be necessary for some estimations
149
+ to,
150
+ value,
151
+ data,
152
+ });
153
+
154
+ return new BigNumber(gasEtimation.toString());
155
+ } catch (e) {
156
+ log("error", "EVM Family: Gas Estimation Error", e);
157
+ throw new GasEstimationError();
158
+ }
159
+ });
160
+
161
+ /**
162
+ * Get an estimation of fees on the network
163
+ */
164
+ export const getFeesEstimation = (currency: CryptoCurrency): Promise<FeeData> =>
165
+ withApi(currency, async (api) => {
166
+ const block = await api.getBlock("latest");
167
+ const currencySupports1559 = Boolean(block.baseFeePerGas);
168
+
169
+ const feeData = await (async () => {
170
+ if (currencySupports1559) {
171
+ const feeHistory: FeeHistory = await api.send("eth_feeHistory", [
172
+ "0x5", // Fetching the history for 5 blocks
173
+ "latest", // from the latest block
174
+ [50], // 50% percentile sample
175
+ ]);
176
+ // Taking the average priority fee used on the last 5 blocks
177
+ const maxPriorityFeeAverage = feeHistory.reward
178
+ .reduce(
179
+ (acc, [curr]) => acc.plus(new BigNumber(curr)),
180
+ new BigNumber(0)
181
+ )
182
+ .dividedToIntegerBy(feeHistory.reward.length);
183
+
184
+ // A maxPriorityFeePerGas too low might make a transaction stuck forever
185
+ // As a safety measure, if maxPriorityFeePerGas is zero
186
+ // we enforce a 1 Gwei value
187
+ const maxPriorityFeePerGas = maxPriorityFeeAverage.isZero()
188
+ ? new BigNumber(1e9) // 1 Gwei
189
+ : maxPriorityFeeAverage;
190
+
191
+ const nextBaseFee = new BigNumber(
192
+ feeHistory.baseFeePerGas[feeHistory.baseFeePerGas.length - 1]
193
+ );
194
+
195
+ return {
196
+ maxPriorityFeePerGas,
197
+ maxFeePerGas: nextBaseFee.multipliedBy(2).plus(maxPriorityFeePerGas),
198
+ };
199
+ } else {
200
+ const gasPrice = await api.getGasPrice();
201
+
202
+ return {
203
+ gasPrice,
204
+ };
205
+ }
206
+ })();
207
+
208
+ return {
209
+ maxFeePerGas: feeData.maxFeePerGas
210
+ ? new BigNumber(feeData.maxFeePerGas.toString())
211
+ : null,
212
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
213
+ ? new BigNumber(feeData.maxPriorityFeePerGas.toString())
214
+ : null,
215
+ gasPrice: feeData.gasPrice
216
+ ? new BigNumber(feeData.gasPrice.toString())
217
+ : null,
218
+ };
219
+ });
220
+
221
+ /**
222
+ * Broadcast a serialized transaction
223
+ */
224
+ export const broadcastTransaction = (
225
+ currency: CryptoCurrency,
226
+ signedTxHex: string
227
+ ): Promise<ethers.providers.TransactionResponse> =>
228
+ withApi(
229
+ currency,
230
+ async (api) => {
231
+ try {
232
+ return await api.sendTransaction(signedTxHex);
233
+ } catch (e) {
234
+ if ((e as Error & { code: string }).code === "INSUFFICIENT_FUNDS") {
235
+ log("error", "EVM Family: Wrong estimation of fees", e);
236
+ throw new InsufficientFunds();
237
+ }
238
+ throw e;
239
+ }
240
+ },
241
+ 0
242
+ );
243
+
244
+ /**
245
+ * Get the informations about a block by block height
246
+ */
247
+ export const getBlock = (
248
+ currency: CryptoCurrency,
249
+ blockHeight: number
250
+ ): Promise<ethers.providers.Block> =>
251
+ withApi(currency, async (api) => {
252
+ return api.getBlock(blockHeight);
253
+ });
254
+
255
+ /**
256
+ * Get account balances and nonce
257
+ */
258
+ export const getSubAccount: (
259
+ currency: CryptoCurrency,
260
+ addr: string
261
+ ) => Promise<{
262
+ blockHeight: number;
263
+ balance: BigNumber;
264
+ nonce: number;
265
+ }> = async (currency, addr) =>
266
+ withApi(currency, async (api) => {
267
+ const [balance, nonce, blockHeight] = await Promise.all([
268
+ getCoinBalance(currency, addr),
269
+ getTransactionCount(currency, addr),
270
+ api.getBlockNumber(),
271
+ ]);
272
+
273
+ return {
274
+ blockHeight,
275
+ balance: new BigNumber(balance.toString()),
276
+ nonce,
277
+ };
278
+ });
279
+
280
+ /**
281
+ * ⚠️ Blockchain specific
282
+ *
283
+ * For a layer 2 like Optimism, additional fees are needed in order to
284
+ * take into account layer 1 settlement estimated cost.
285
+ * This gas price is served through a smart contract oracle.
286
+ *
287
+ * @see https://help.optimism.io/hc/en-us/articles/4411895794715-How-do-transaction-fees-on-Optimism-work-
288
+ */
289
+ export const getOptimismAdditionalFees = makeLRUCache<
290
+ [CryptoCurrency, EvmTransaction],
291
+ BigNumber
292
+ >(
293
+ async (currency, transaction) =>
294
+ withApi(currency, async (api) => {
295
+ if (!["optimism", "optimism_goerli"].includes(currency.id)) {
296
+ return new BigNumber(0);
297
+ }
298
+
299
+ // Fake signature is added to get the best approximation possible for the gas on L1
300
+ const serializedTransaction = (() => {
301
+ try {
302
+ return getSerializedTransaction(transaction, {
303
+ r: "0xffffffffffffffffffffffffffffffffffffffff",
304
+ s: "0xffffffffffffffffffffffffffffffffffffffff",
305
+ v: 0,
306
+ });
307
+ } catch (e) {
308
+ return null;
309
+ }
310
+ })();
311
+ if (!serializedTransaction) {
312
+ return new BigNumber(0);
313
+ }
314
+
315
+ const optimismGasOracle = new ethers.Contract(
316
+ // contract address provided here
317
+ // @see https://community.optimism.io/docs/developers/build/transaction-fees/#displaying-fees-to-users
318
+ "0x420000000000000000000000000000000000000F",
319
+ OptimismGasPriceOracleAbi,
320
+ api
321
+ );
322
+ const additionalL1Fees = await optimismGasOracle.getL1Fee(
323
+ serializedTransaction
324
+ );
325
+ return new BigNumber(additionalL1Fees.toString());
326
+ }),
327
+ (currency, transaction) => {
328
+ const serializedTransaction = (() => {
329
+ try {
330
+ return getSerializedTransaction(transaction);
331
+ } catch (e) {
332
+ return null;
333
+ }
334
+ })();
335
+
336
+ return "getOptimismL1BaseFee_" + currency.id + "_" + serializedTransaction;
337
+ },
338
+ { ttl: 15 * 1000 } // preventing rate limit by caching this for at least 15sec
339
+ );
340
+
341
+ export default {
342
+ DEFAULT_RETRIES_RPC_METHODS,
343
+ RPC_TIMEOUT,
344
+ withApi,
345
+ getBalanceAndBlock,
346
+ getTransaction,
347
+ getCoinBalance,
348
+ getTransactionCount,
349
+ getGasEstimation,
350
+ getFeesEstimation,
351
+ broadcastTransaction,
352
+ getBlock,
353
+ getOptimismAdditionalFees,
354
+ };
@@ -0,0 +1,5 @@
1
+ /** ⚠️ keep this order of import. @see https://docs.ethers.io/v5/cookbook/react-native/#cookbook-reactnative ⚠️ */
2
+ import "react-native-get-random-values";
3
+ import "@ethersproject/shims";
4
+ export * from "./rpc.common";
5
+ export * as default from "./rpc.common";
package/src/api/rpc.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./rpc.common";
2
+ export * as default from "./rpc.common";
@@ -0,0 +1,77 @@
1
+ import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
2
+ import {
3
+ DeviceCommunication,
4
+ makeAccountBridgeReceive,
5
+ makeScanAccounts,
6
+ } from "@ledgerhq/coin-framework/bridge/jsHelpers";
7
+ import type { AccountBridge, CurrencyBridge } from "@ledgerhq/types-live";
8
+ import { broadcast } from "../broadcast";
9
+ import { createTransaction } from "../createTransaction";
10
+ import { estimateMaxSpendable } from "../estimateMaxSpendable";
11
+ import { getTransactionStatus } from "../getTransactionStatus";
12
+ import getAddress from "../hw-getAddress";
13
+ import { hydrate, preload } from "../preload";
14
+ import { prepareTransaction } from "../prepareTransaction";
15
+ import { buildSignOperation } from "../signOperation";
16
+ import { getAccountShape, sync } from "../synchronization";
17
+ import type { Transaction as EvmTransaction, Transaction } from "../types";
18
+
19
+ const updateTransaction: AccountBridge<EvmTransaction>["updateTransaction"] = (
20
+ transaction,
21
+ patch
22
+ ) => {
23
+ return { ...transaction, ...patch } as EvmTransaction;
24
+ };
25
+
26
+ export function buildCurrencyBridge(
27
+ deviceCommunication: DeviceCommunication
28
+ ): CurrencyBridge {
29
+ const scanAccounts = makeScanAccounts({
30
+ getAccountShape,
31
+ deviceCommunication,
32
+ getAddressFn: getAddress,
33
+ });
34
+
35
+ return {
36
+ preload,
37
+ hydrate,
38
+ scanAccounts,
39
+ };
40
+ }
41
+
42
+ export function buildAccountBridge(
43
+ deviceCommunication: DeviceCommunication
44
+ ): AccountBridge<Transaction> {
45
+ const receive = makeAccountBridgeReceive(
46
+ getAddressWrapper(getAddress),
47
+ deviceCommunication
48
+ );
49
+ const signOperation = buildSignOperation(deviceCommunication);
50
+
51
+ return {
52
+ createTransaction,
53
+ updateTransaction,
54
+ prepareTransaction,
55
+ getTransactionStatus,
56
+ sync,
57
+ receive,
58
+ signOperation,
59
+ broadcast,
60
+ estimateMaxSpendable,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * FIXME: unsued network and cacheFn are passed to createBridges because of how the
66
+ * libs/ledger-live-common/scripts/sync-families-dispatch.mjs script works.
67
+ */
68
+ export function createBridges(
69
+ deviceCommunication: DeviceCommunication,
70
+ _network: unknown,
71
+ _cacheFn: unknown
72
+ ) {
73
+ return {
74
+ currencyBridge: buildCurrencyBridge(deviceCommunication),
75
+ accountBridge: buildAccountBridge(deviceCommunication),
76
+ };
77
+ }