@toruslabs/ethereum-controllers 4.1.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 (88) hide show
  1. package/dist/ethereumControllers.cjs.js +6153 -0
  2. package/dist/ethereumControllers.cjs.js.map +1 -0
  3. package/dist/ethereumControllers.esm.js +5570 -0
  4. package/dist/ethereumControllers.esm.js.map +1 -0
  5. package/dist/ethereumControllers.umd.min.js +3 -0
  6. package/dist/ethereumControllers.umd.min.js.LICENSE.txt +38 -0
  7. package/dist/ethereumControllers.umd.min.js.map +1 -0
  8. package/dist/types/Account/AccountTrackerController.d.ts +35 -0
  9. package/dist/types/Block/PollingBlockTracker.d.ts +14 -0
  10. package/dist/types/Currency/CurrencyController.d.ts +30 -0
  11. package/dist/types/Gas/GasFeeController.d.ts +64 -0
  12. package/dist/types/Gas/IGasFeeController.d.ts +49 -0
  13. package/dist/types/Gas/gasUtil.d.ts +21 -0
  14. package/dist/types/Keyring/KeyringController.d.ts +20 -0
  15. package/dist/types/Message/AbstractMessageController.d.ts +36 -0
  16. package/dist/types/Message/DecryptMessageController.d.ts +20 -0
  17. package/dist/types/Message/EncryptionPublicKeyController.d.ts +20 -0
  18. package/dist/types/Message/MessageController.d.ts +20 -0
  19. package/dist/types/Message/PersonalMessageController.d.ts +20 -0
  20. package/dist/types/Message/TypedMessageController.d.ts +21 -0
  21. package/dist/types/Message/utils.d.ts +10 -0
  22. package/dist/types/Network/NetworkController.d.ts +40 -0
  23. package/dist/types/Network/createEthereumMiddleware.d.ts +66 -0
  24. package/dist/types/Network/createJsonRpcClient.d.ts +9 -0
  25. package/dist/types/Nfts/INftsController.d.ts +10 -0
  26. package/dist/types/Nfts/NftHandler.d.ts +35 -0
  27. package/dist/types/Nfts/NftsController.d.ts +40 -0
  28. package/dist/types/Preferences/PreferencesController.d.ts +53 -0
  29. package/dist/types/Tokens/ITokensController.d.ts +10 -0
  30. package/dist/types/Tokens/TokenHandler.d.ts +20 -0
  31. package/dist/types/Tokens/TokenRatesController.d.ts +42 -0
  32. package/dist/types/Tokens/TokensController.d.ts +42 -0
  33. package/dist/types/Transaction/NonceTracker.d.ts +37 -0
  34. package/dist/types/Transaction/PendingTransactionTracker.d.ts +32 -0
  35. package/dist/types/Transaction/TransactionController.d.ts +67 -0
  36. package/dist/types/Transaction/TransactionGasUtil.d.ts +21 -0
  37. package/dist/types/Transaction/TransactionStateHistoryHelper.d.ts +16 -0
  38. package/dist/types/Transaction/TransactionStateManager.d.ts +30 -0
  39. package/dist/types/Transaction/TransactionUtils.d.ts +70 -0
  40. package/dist/types/index.d.ts +43 -0
  41. package/dist/types/utils/abiDecoder.d.ts +17 -0
  42. package/dist/types/utils/abis.d.ts +84 -0
  43. package/dist/types/utils/constants.d.ts +81 -0
  44. package/dist/types/utils/contractAddresses.d.ts +1 -0
  45. package/dist/types/utils/conversionUtils.d.ts +42 -0
  46. package/dist/types/utils/helpers.d.ts +24 -0
  47. package/dist/types/utils/interfaces.d.ts +384 -0
  48. package/package.json +71 -0
  49. package/src/Account/AccountTrackerController.ts +157 -0
  50. package/src/Block/PollingBlockTracker.ts +89 -0
  51. package/src/Currency/CurrencyController.ts +117 -0
  52. package/src/Gas/GasFeeController.ts +254 -0
  53. package/src/Gas/IGasFeeController.ts +56 -0
  54. package/src/Gas/gasUtil.ts +163 -0
  55. package/src/Keyring/KeyringController.ts +118 -0
  56. package/src/Message/AbstractMessageController.ts +136 -0
  57. package/src/Message/DecryptMessageController.ts +81 -0
  58. package/src/Message/EncryptionPublicKeyController.ts +83 -0
  59. package/src/Message/MessageController.ts +74 -0
  60. package/src/Message/PersonalMessageController.ts +74 -0
  61. package/src/Message/TypedMessageController.ts +112 -0
  62. package/src/Message/utils.ts +107 -0
  63. package/src/Network/NetworkController.ts +184 -0
  64. package/src/Network/createEthereumMiddleware.ts +307 -0
  65. package/src/Network/createJsonRpcClient.ts +59 -0
  66. package/src/Nfts/INftsController.ts +13 -0
  67. package/src/Nfts/NftHandler.ts +191 -0
  68. package/src/Nfts/NftsController.ts +230 -0
  69. package/src/Preferences/PreferencesController.ts +409 -0
  70. package/src/Tokens/ITokensController.ts +13 -0
  71. package/src/Tokens/TokenHandler.ts +60 -0
  72. package/src/Tokens/TokenRatesController.ts +134 -0
  73. package/src/Tokens/TokensController.ts +278 -0
  74. package/src/Transaction/NonceTracker.ts +152 -0
  75. package/src/Transaction/PendingTransactionTracker.ts +235 -0
  76. package/src/Transaction/TransactionController.ts +558 -0
  77. package/src/Transaction/TransactionGasUtil.ts +74 -0
  78. package/src/Transaction/TransactionStateHistoryHelper.ts +41 -0
  79. package/src/Transaction/TransactionStateManager.ts +315 -0
  80. package/src/Transaction/TransactionUtils.ts +333 -0
  81. package/src/index.ts +45 -0
  82. package/src/utils/abiDecoder.ts +195 -0
  83. package/src/utils/abis.ts +677 -0
  84. package/src/utils/constants.ts +379 -0
  85. package/src/utils/contractAddresses.ts +21 -0
  86. package/src/utils/conversionUtils.ts +269 -0
  87. package/src/utils/helpers.ts +177 -0
  88. package/src/utils/interfaces.ts +454 -0
@@ -0,0 +1,558 @@
1
+ import { Common, Hardfork } from "@ethereumjs/common";
2
+ import { TransactionFactory, TypedTransaction } from "@ethereumjs/tx";
3
+ import { addHexPrefix, stripHexPrefix } from "@ethereumjs/util";
4
+ import { providerErrors, rpcErrors } from "@metamask/rpc-errors";
5
+ import {
6
+ ITransactionController,
7
+ TRANSACTION_TYPES,
8
+ TransactionConfig,
9
+ TransactionState,
10
+ TransactionStatus,
11
+ TX_CONFIRMED_EVENT_TYPE,
12
+ TX_DROPPED_EVENT_TYPE,
13
+ TX_EVENTS,
14
+ TX_FAILED_EVENT_TYPE,
15
+ TX_WARNING_EVENT_TYPE,
16
+ } from "@toruslabs/base-controllers";
17
+ import { JRPCRequest, SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
18
+ import BigNumber from "bignumber.js";
19
+ import { keccak256 } from "ethers";
20
+ import log from "loglevel";
21
+
22
+ import PollingBlockTracker from "../Block/PollingBlockTracker";
23
+ import GasFeeController from "../Gas/GasFeeController";
24
+ import { type EthereumGasFeeEstimates, EthereumLegacyGasFeeEstimates } from "../Gas/IGasFeeController";
25
+ import KeyringController from "../Keyring/KeyringController";
26
+ import NetworkController from "../Network/NetworkController";
27
+ import PreferencesController from "../Preferences/PreferencesController";
28
+ import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP, GAS_ESTIMATE_TYPES, METHOD_TYPES, TRANSACTION_ENVELOPE_TYPES } from "../utils/constants";
29
+ import { decGWEIToHexWEI } from "../utils/conversionUtils";
30
+ import { bnLessThan, GAS_LIMITS, getChainType } from "../utils/helpers";
31
+ import {
32
+ EthereumBlock,
33
+ EthereumTransactionMeta,
34
+ NonceLockRes,
35
+ TransactionParams,
36
+ TransactionReceipt,
37
+ UserRequestApprovalParams,
38
+ } from "../utils/interfaces";
39
+ import NonceTracker from "./NonceTracker";
40
+ import PendingTransactionTracker from "./PendingTransactionTracker";
41
+ import TransactionGasUtil from "./TransactionGasUtil";
42
+ import TransactionStateManager from "./TransactionStateManager";
43
+ import { determineTransactionType, isEIP1559Transaction, normalizeTxParameters, validateTxParameters } from "./TransactionUtils";
44
+
45
+ export default class TransactionController extends TransactionStateManager implements ITransactionController<TransactionParams> {
46
+ getSelectedAddress: PreferencesController["getSelectedAddress"];
47
+
48
+ getEIP1559GasFeeEstimates: GasFeeController["fetchGasFeeEstimates"];
49
+
50
+ public nonceTracker: NonceTracker;
51
+
52
+ public pendingTxTracker: PendingTransactionTracker;
53
+
54
+ public txGasUtil: TransactionGasUtil;
55
+
56
+ private _getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
57
+
58
+ private _getCurrentAccountEIP1559Compatibility: (address?: string) => Promise<boolean>;
59
+
60
+ private getProviderConfig: NetworkController["getProviderConfig"];
61
+
62
+ private signEthTx: KeyringController["signTransaction"];
63
+
64
+ private provider: SafeEventEmitterProvider;
65
+
66
+ private blockTracker: PollingBlockTracker;
67
+
68
+ private inProcessOfSigning: Set<string> = new Set();
69
+
70
+ constructor({
71
+ config,
72
+ state,
73
+ provider,
74
+ blockTracker,
75
+ signEthTx,
76
+ getCurrentChainId,
77
+ getCurrentNetworkEIP1559Compatibility,
78
+ getProviderConfig,
79
+ getCurrentAccountEIP1559Compatibility,
80
+ getSelectedAddress,
81
+ getEIP1559GasFeeEstimates,
82
+ }: {
83
+ config?: Partial<TransactionConfig>;
84
+ state?: Partial<TransactionState<TransactionParams, EthereumTransactionMeta>>;
85
+ provider: SafeEventEmitterProvider;
86
+ blockTracker: PollingBlockTracker;
87
+ signEthTx: KeyringController["signTransaction"];
88
+ getCurrentChainId: NetworkController["getNetworkIdentifier"];
89
+ getProviderConfig: NetworkController["getProviderConfig"];
90
+ getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
91
+ getCurrentAccountEIP1559Compatibility: (address?: string) => Promise<boolean>; // used only if keyring supports EIP-1559
92
+ getSelectedAddress: PreferencesController["getSelectedAddress"];
93
+ getEIP1559GasFeeEstimates: GasFeeController["fetchGasFeeEstimates"];
94
+ }) {
95
+ super({ config, state, getCurrentChainId });
96
+ this.blockTracker = blockTracker;
97
+ this.getProviderConfig = getProviderConfig;
98
+ this._getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility;
99
+ this._getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility;
100
+ this.getSelectedAddress = getSelectedAddress;
101
+ this.getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
102
+ this.signEthTx = signEthTx;
103
+ this.provider = provider;
104
+ this.txGasUtil = new TransactionGasUtil(this.provider, this.blockTracker);
105
+ this.nonceTracker = new NonceTracker({
106
+ provider,
107
+ blockTracker,
108
+ getConfirmedTransactions: this.getConfirmedTransactions.bind(this),
109
+ getPendingTransactions: this.getSubmittedTransactions.bind(this), // nonce tracker should only care about submitted transactions
110
+ });
111
+ this.pendingTxTracker = new PendingTransactionTracker({
112
+ provider,
113
+ nonceTracker: this.nonceTracker,
114
+ getPendingTransactions: this.getPendingTransactions.bind(this), // pending tx tracker should only care about submitted and approved transactions
115
+ getConfirmedTransactions: this.getConfirmedTransactions.bind(this),
116
+ approveTransaction: this.approveTransaction.bind(this),
117
+ publishTransaction: (rawTx) => this.provider.request<[string], string>({ method: METHOD_TYPES.ETH_SEND_RAW_TRANSACTION, params: [rawTx] }),
118
+ });
119
+ this._setupListeners();
120
+ }
121
+
122
+ addTransactionUnapproved(txMeta: EthereumTransactionMeta) {
123
+ this.addTransactionToState(txMeta);
124
+ this.emit(`${txMeta.id}:unapproved`, txMeta);
125
+ }
126
+
127
+ async addNewUnapprovedTransaction(
128
+ txParams: TransactionParams,
129
+ req: JRPCRequest<TransactionParams> & UserRequestApprovalParams & { origin: string }
130
+ ): Promise<string> {
131
+ const txMeta = await this.createTransaction(txParams, req);
132
+ return this.processApproval(txMeta);
133
+ }
134
+
135
+ async processApproval(txMeta: EthereumTransactionMeta): Promise<string> {
136
+ return new Promise((resolve, reject) => {
137
+ const handleFinished = (msg: EthereumTransactionMeta) => {
138
+ if (msg.status === TransactionStatus.rejected) {
139
+ return reject(providerErrors.userRejectedRequest(`Transaction Signature: User denied message signature`));
140
+ }
141
+ if (msg.status === TransactionStatus.failed) {
142
+ return reject(rpcErrors.internal(`Transaction Signature: failed to sign message ${msg.error}`));
143
+ }
144
+ if (msg.status === TransactionStatus.submitted) {
145
+ return resolve(msg.transactionHash);
146
+ }
147
+
148
+ return reject(rpcErrors.internal(`Transaction Signature: Unknown problem: ${JSON.stringify(txMeta.transaction)}`));
149
+ };
150
+ this.once(`${txMeta.id}:finished`, handleFinished);
151
+ });
152
+ }
153
+
154
+ async approveTransaction(transactionID: string): Promise<void> {
155
+ const txMeta = this.getTransaction(transactionID);
156
+ if (this.inProcessOfSigning.has(transactionID)) {
157
+ return;
158
+ }
159
+ this.inProcessOfSigning.add(transactionID);
160
+ let nonceLock: NonceLockRes;
161
+ try {
162
+ this.setTxStatusApproved(transactionID);
163
+ const fromAddress = txMeta.transaction.from;
164
+ const { customNonceValue } = txMeta.transaction;
165
+ const customNonceValueNumber = Number(customNonceValue);
166
+ nonceLock = await this.nonceTracker.getNonceLock(fromAddress);
167
+ // add nonce to txParams
168
+ // if txMeta has previousGasParams then it is a retry at same nonce with
169
+ // higher gas settings and therefor the nonce should not be recalculated
170
+ const nonce = nonceLock.nextNonce;
171
+ const customOrNonce = customNonceValueNumber === 0 ? customNonceValue : customNonceValue || nonce;
172
+ txMeta.transaction.nonce = addHexPrefix(customOrNonce.toString(16));
173
+ // add nonce debugging information to txMeta
174
+ txMeta.nonceDetails = nonceLock.nonceDetails;
175
+ this.updateTransactionInState(txMeta, "transactions#approveTransaction");
176
+ // sign transaction
177
+ const rawTx = await this.signTransaction(transactionID);
178
+ await this.publishTransaction(transactionID, rawTx);
179
+ nonceLock.releaseLock();
180
+ } catch (err) {
181
+ try {
182
+ this.setTxStatusFailed(transactionID, err as Error);
183
+ } catch (err2) {
184
+ log.error(err2);
185
+ }
186
+ // must set transaction to submitted/failed before releasing lock
187
+ if (nonceLock) {
188
+ nonceLock.releaseLock();
189
+ }
190
+ // continue with error chain
191
+ throw err;
192
+ } finally {
193
+ this.inProcessOfSigning.delete(transactionID);
194
+ }
195
+ }
196
+
197
+ async signTransaction(txId: string): Promise<string> {
198
+ const txMeta = this.getTransaction(txId);
199
+ const chainId = this.getCurrentChainId();
200
+ const type = isEIP1559Transaction(txMeta) ? TRANSACTION_ENVELOPE_TYPES.FEE_MARKET : TRANSACTION_ENVELOPE_TYPES.LEGACY;
201
+ const txParams: TransactionParams = {
202
+ ...txMeta.transaction,
203
+ type,
204
+ chainId,
205
+ gasLimit: txMeta.transaction.gas,
206
+ };
207
+ const fromAddress = txParams.from;
208
+ const common = await this.getCommonConfiguration(fromAddress);
209
+ const unsignedEthTx = TransactionFactory.fromTxData(txParams, { common });
210
+ const signedEthTx = await this.signEthTx<TypedTransaction, TypedTransaction>(unsignedEthTx, fromAddress);
211
+ txMeta.r = addHexPrefix(signedEthTx.r.toString(16));
212
+ txMeta.s = addHexPrefix(signedEthTx.s.toString(16));
213
+ txMeta.v = addHexPrefix(signedEthTx.v.toString(16));
214
+ this.updateTransactionInState(txMeta, "transactions#signTransaction: add r, s, v values");
215
+ this.setTxStatusSigned(txId);
216
+ const rawTx = addHexPrefix(Buffer.from(signedEthTx.serialize()).toString("hex"));
217
+ return rawTx;
218
+ }
219
+
220
+ async publishTransaction(txId: string, rawTx: string): Promise<void> {
221
+ const txMeta = this.getTransaction(txId);
222
+ txMeta.rawTransaction = rawTx;
223
+ this.updateTransactionInState(txMeta, "transactions#publishTransaction");
224
+ let txHash: string;
225
+ try {
226
+ txHash = await this.provider.request<[string], string>({ method: METHOD_TYPES.ETH_SEND_RAW_TRANSACTION, params: [rawTx] });
227
+ } catch (error) {
228
+ if ((error as Error).message.toLowerCase().includes("known transaction")) {
229
+ txHash = keccak256(addHexPrefix(rawTx));
230
+ txHash = addHexPrefix(txHash);
231
+ } else {
232
+ throw error;
233
+ }
234
+ }
235
+ this.setTxHash(txId, txHash);
236
+ this.setTxStatusSubmitted(txId);
237
+ }
238
+
239
+ async confirmTransaction(params: TX_CONFIRMED_EVENT_TYPE): Promise<void> {
240
+ const { txId, txReceipt } = params as TX_CONFIRMED_EVENT_TYPE & {
241
+ baseFeePerGas?: string;
242
+ blockTimestamp?: string;
243
+ txReceipt: TransactionReceipt;
244
+ };
245
+ log.info(params, "confirm params");
246
+ const txMeta = this.getTransaction(txId);
247
+ if (!txMeta) return;
248
+ try {
249
+ txMeta.txReceipt = {
250
+ ...txReceipt,
251
+ };
252
+ this.setTxStatusConfirmed(txId);
253
+ this.markNonceDuplicatesDropped(txId);
254
+ this.updateTransactionInState(txMeta, "transactions#confirmTransaction - add txReceipt");
255
+ } catch (error) {
256
+ log.error(error);
257
+ }
258
+ }
259
+
260
+ cancelTransaction?(transactionID: string): Promise<void> {
261
+ throw new Error(`Method not implemented. ${transactionID}`);
262
+ }
263
+
264
+ async getEIP1559Compatibility(fromAddress?: string) {
265
+ const currentNetworkIsCompatible = await this._getCurrentNetworkEIP1559Compatibility();
266
+ const fromAccountIsCompatible = await this._getCurrentAccountEIP1559Compatibility(fromAddress);
267
+ return currentNetworkIsCompatible && fromAccountIsCompatible;
268
+ }
269
+
270
+ async addTransactionGasDefaults(txMeta: EthereumTransactionMeta) {
271
+ let updateTxMeta = txMeta;
272
+ try {
273
+ updateTxMeta = await this.addTxGasDefaults(txMeta);
274
+ } catch (error) {
275
+ log.warn(error);
276
+ updateTxMeta = this.getTransaction(txMeta.id);
277
+ updateTxMeta.loadingDefaults = false;
278
+ this.updateTransactionInState(txMeta, "Failed to calculate gas defaults.");
279
+ throw error;
280
+ }
281
+ updateTxMeta.loadingDefaults = false;
282
+
283
+ this.updateTransactionInState(updateTxMeta, "Added new unapproved transaction.");
284
+
285
+ return updateTxMeta;
286
+ }
287
+
288
+ async addTxGasDefaults(txMeta: EthereumTransactionMeta) {
289
+ const eip1559Compatibility = txMeta.transaction.type !== TRANSACTION_ENVELOPE_TYPES.LEGACY && (await this.getEIP1559Compatibility());
290
+
291
+ const {
292
+ gasPrice: defaultGasPrice,
293
+ maxFeePerGas: defaultMaxFeePerGas,
294
+ maxPriorityFeePerGas: defaultMaxPriorityFeePerGas,
295
+ } = await this.getDefaultGasFees(txMeta, eip1559Compatibility);
296
+ const { gasLimit: defaultGasLimit, simulationFails } = await this.getDefaultGasLimit(txMeta);
297
+
298
+ txMeta = this.getTransaction(txMeta.id);
299
+ if (simulationFails) {
300
+ txMeta.simulationFails = simulationFails;
301
+ }
302
+
303
+ if (eip1559Compatibility) {
304
+ // If the dapp has suggested a gas price, but no maxFeePerGas or maxPriorityFeePerGas
305
+ // then we set maxFeePerGas and maxPriorityFeePerGas to the suggested gasPrice.
306
+ if (txMeta.transaction.gasPrice && !txMeta.transaction.maxFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
307
+ txMeta.transaction.maxFeePerGas = txMeta.transaction.gasPrice;
308
+ // If the dapp has suggested a gas price, but no maxFeePerGas or maxPriorityFeePerGas
309
+ // then we set maxFeePerGas to the suggested gasPrice.
310
+
311
+ txMeta.transaction.maxPriorityFeePerGas = bnLessThan(
312
+ typeof defaultMaxPriorityFeePerGas === "string" ? stripHexPrefix(defaultMaxPriorityFeePerGas) : defaultMaxPriorityFeePerGas,
313
+ typeof txMeta.transaction.gasPrice === "string" ? stripHexPrefix(txMeta.transaction.gasPrice) : txMeta.transaction.gasPrice
314
+ )
315
+ ? defaultMaxPriorityFeePerGas
316
+ : txMeta.transaction.gasPrice;
317
+ } else {
318
+ if (defaultMaxFeePerGas && !txMeta.transaction.maxFeePerGas) {
319
+ // If the dapp has not set the gasPrice or the maxFeePerGas, then we set maxFeePerGas
320
+ // with the one returned by the gasFeeController, if that is available.
321
+ txMeta.transaction.maxFeePerGas = defaultMaxFeePerGas;
322
+ }
323
+
324
+ if (defaultMaxPriorityFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
325
+ // If the dapp has not set the gasPrice or the maxPriorityFeePerGas, then we set maxPriorityFeePerGas
326
+ // with the one returned by the gasFeeController, if that is available.
327
+ txMeta.transaction.maxPriorityFeePerGas = defaultMaxPriorityFeePerGas;
328
+ }
329
+
330
+ if (defaultGasPrice && !txMeta.transaction.maxFeePerGas) {
331
+ // If the dapp has not set the gasPrice or the maxFeePerGas, and no maxFeePerGas is available
332
+ // from the gasFeeController, then we set maxFeePerGas to the defaultGasPrice, assuming it is
333
+ // available.
334
+ txMeta.transaction.maxFeePerGas = defaultGasPrice;
335
+ }
336
+
337
+ if (txMeta.transaction.maxFeePerGas && !txMeta.transaction.maxPriorityFeePerGas) {
338
+ // If the dapp has not set the gasPrice or the maxPriorityFeePerGas, and no maxPriorityFeePerGas is
339
+ // available from the gasFeeController, then we set maxPriorityFeePerGas to
340
+ // txMeta.transaction.maxFeePerGas, which will either be the gasPrice from the controller, the maxFeePerGas
341
+ // set by the dapp, or the maxFeePerGas from the controller.
342
+ txMeta.transaction.maxPriorityFeePerGas = txMeta.transaction.maxFeePerGas;
343
+ }
344
+ }
345
+
346
+ // We remove the gasPrice param entirely when on an eip1559 compatible network
347
+
348
+ delete txMeta.transaction.gasPrice;
349
+ } else {
350
+ // We ensure that maxFeePerGas and maxPriorityFeePerGas are not in the transaction params
351
+ // when not on a EIP1559 compatible network
352
+
353
+ delete txMeta.transaction.maxPriorityFeePerGas;
354
+ delete txMeta.transaction.maxFeePerGas;
355
+ }
356
+
357
+ // If we have gotten to this point, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas are
358
+ // set on transaction, it means that either we are on a non-EIP1559 network and the dapp didn't suggest
359
+ // a gas price, or we are on an EIP1559 network, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas
360
+ // were available from either the dapp or the network.
361
+ if (defaultGasPrice && !txMeta.transaction.gasPrice && !txMeta.transaction.maxPriorityFeePerGas && !txMeta.transaction.maxFeePerGas) {
362
+ txMeta.transaction.gasPrice = defaultGasPrice;
363
+ }
364
+
365
+ if (defaultGasLimit && !txMeta.transaction.gas) {
366
+ txMeta.transaction.gas = defaultGasLimit;
367
+ }
368
+ return txMeta;
369
+ }
370
+
371
+ setTxHash(txId: string, txHash: string) {
372
+ // Add the tx hash to the persisted meta-tx object
373
+ const txMeta = this.getTransaction(txId);
374
+ txMeta.transactionHash = txHash;
375
+ this.updateTransactionInState(txMeta, "transactions#setTxHash");
376
+ }
377
+
378
+ getUnapprovedTxCount = () => Object.keys(this.getUnapprovedTxList()).length;
379
+
380
+ getPendingTxCount = (account?: string) => this.getPendingTransactions(account).length;
381
+
382
+ async getDefaultGasFees(
383
+ txMeta: EthereumTransactionMeta,
384
+ eip1559Compatibility: boolean
385
+ ): Promise<{ maxFeePerGas?: string; maxPriorityFeePerGas?: string; gasPrice?: string }> {
386
+ if (
387
+ (!eip1559Compatibility && txMeta.transaction.gasPrice) ||
388
+ (eip1559Compatibility && txMeta.transaction.maxFeePerGas && txMeta.transaction.maxPriorityFeePerGas)
389
+ ) {
390
+ return {};
391
+ }
392
+
393
+ try {
394
+ const { gasFeeEstimates, gasEstimateType } = await this.getEIP1559GasFeeEstimates();
395
+ if (eip1559Compatibility && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
396
+ // this is in dec gwei
397
+ const { medium: { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = {} } = <EthereumGasFeeEstimates>gasFeeEstimates;
398
+
399
+ if (suggestedMaxPriorityFeePerGas && suggestedMaxFeePerGas) {
400
+ return {
401
+ // send to controller in hex wei
402
+ maxFeePerGas: addHexPrefix(decGWEIToHexWEI(new BigNumber(suggestedMaxFeePerGas)).toString(16)),
403
+ maxPriorityFeePerGas: addHexPrefix(decGWEIToHexWEI(new BigNumber(suggestedMaxPriorityFeePerGas)).toString(16)),
404
+ };
405
+ }
406
+ } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
407
+ const { medium } = <EthereumLegacyGasFeeEstimates>gasFeeEstimates;
408
+ // The LEGACY type includes low, medium and high estimates of
409
+ // gas price values.
410
+ return {
411
+ gasPrice: addHexPrefix(decGWEIToHexWEI(new BigNumber(medium)).toString(16)),
412
+ };
413
+ } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
414
+ const { gasPrice } = <{ gasPrice?: string }>gasFeeEstimates;
415
+ // The ETH_GASPRICE type just includes a single gas price property,
416
+ // which we can assume was retrieved from eth_gasPrice
417
+ return {
418
+ gasPrice: addHexPrefix(decGWEIToHexWEI(new BigNumber(gasPrice)).toString(16)),
419
+ };
420
+ }
421
+ } catch (error) {
422
+ log.error(error);
423
+ }
424
+
425
+ const gasPrice = await this.provider.request<never, string>({ method: METHOD_TYPES.ETH_GET_GAS_PRICE });
426
+
427
+ return { gasPrice: gasPrice && addHexPrefix(gasPrice) };
428
+ }
429
+
430
+ private async getDefaultGasLimit(txMeta: EthereumTransactionMeta) {
431
+ const chainId = this.getCurrentChainId();
432
+ const customNetworkGasBuffer = CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId];
433
+ const chainType = getChainType(chainId);
434
+
435
+ if (txMeta.transaction.gas) {
436
+ return {};
437
+ }
438
+ if (txMeta.transaction.to && txMeta.transactionCategory === TRANSACTION_TYPES.SENT_ETHER && chainType !== "custom" && !txMeta.transaction.data) {
439
+ // This is a standard ether simple send, gas requirement is exactly 21k
440
+ return { gasLimit: GAS_LIMITS.SIMPLE };
441
+ }
442
+
443
+ const { blockGasLimit, estimatedGasHex, simulationFails } = await this.txGasUtil.analyzeGasUsage(txMeta);
444
+
445
+ // add additional gas buffer to our estimation for safety
446
+ const gasLimit = this.txGasUtil.addGasBuffer(addHexPrefix(estimatedGasHex), blockGasLimit, customNetworkGasBuffer);
447
+ return { gasLimit, simulationFails };
448
+ }
449
+
450
+ private async createTransaction(
451
+ txParameters: TransactionParams,
452
+ req: JRPCRequest<TransactionParams> & UserRequestApprovalParams
453
+ ): Promise<EthereumTransactionMeta> {
454
+ const normalizedTxParameters = normalizeTxParameters(txParameters);
455
+ const eip1559Compatibility = await this.getEIP1559Compatibility(txParameters.from);
456
+ validateTxParameters(normalizedTxParameters, eip1559Compatibility);
457
+
458
+ let txMeta = this.generateTxMeta({
459
+ transaction: normalizedTxParameters,
460
+ origin: req.origin,
461
+ });
462
+
463
+ const { type, category, methodParams } = await determineTransactionType(txParameters, this.provider);
464
+ txMeta.type = type;
465
+ txMeta.transactionCategory = category;
466
+ txMeta.methodParams = methodParams;
467
+ txMeta.transaction.value = txMeta.transaction.value ? addHexPrefix(txMeta.transaction.value) : "0x0";
468
+ this.emit(`${txMeta.id}:unapproved`, txMeta);
469
+ txMeta = this.addTransactionToState(txMeta);
470
+ txMeta = await this.addTransactionGasDefaults(txMeta);
471
+
472
+ this.emit(TX_EVENTS.TX_UNAPPROVED, { txMeta, req });
473
+
474
+ return txMeta;
475
+ }
476
+
477
+ private _setupListeners() {
478
+ this.setupBlockTrackerListener();
479
+ this.pendingTxTracker.on(TX_EVENTS.TX_WARNING, (data: TX_WARNING_EVENT_TYPE<TransactionParams, EthereumTransactionMeta>) => {
480
+ this.updateTransactionInState(data.txMeta);
481
+ });
482
+ this.pendingTxTracker.on(TX_EVENTS.TX_DROPPED, (data: TX_DROPPED_EVENT_TYPE) => this.setTxStatusDropped(data.txId));
483
+ this.pendingTxTracker.on(
484
+ TX_EVENTS.TX_BLOCK_UPDATE,
485
+ ({ txMeta, latestBlockNumber }: { txMeta: EthereumTransactionMeta; latestBlockNumber: string; txId: string }) => {
486
+ if (!txMeta.firstRetryBlockNumber) {
487
+ txMeta.firstRetryBlockNumber = latestBlockNumber;
488
+ this.updateTransactionInState(txMeta);
489
+ }
490
+ }
491
+ );
492
+ this.pendingTxTracker.on(TX_EVENTS.TX_RETRY, (txMeta) => {
493
+ if (!("retryCount" in txMeta)) {
494
+ txMeta.retryCount = 0;
495
+ }
496
+ txMeta.retryCount += 1;
497
+ this.updateTransactionInState(txMeta);
498
+ });
499
+
500
+ this.pendingTxTracker.on(TX_EVENTS.TX_FAILED, (data: TX_FAILED_EVENT_TYPE) => {
501
+ this.setTxStatusFailed(data.txId, data.error);
502
+ });
503
+ this.pendingTxTracker.on(TX_EVENTS.TX_CONFIRMED, (data: TX_CONFIRMED_EVENT_TYPE) => this.confirmTransaction(data));
504
+ }
505
+
506
+ private setupBlockTrackerListener() {
507
+ let listenersAreActive = false;
508
+ const latestBlockHandler = this.onLatestBlock.bind(this);
509
+ this.on(TX_EVENTS.TX_STATUS_UPDATE, () => {
510
+ const pendingTxs = this.getPendingTransactions();
511
+ if (!listenersAreActive && pendingTxs.length > 0) {
512
+ this.blockTracker.on("latest", latestBlockHandler);
513
+ listenersAreActive = true;
514
+ } else if (listenersAreActive && !pendingTxs.length) {
515
+ this.blockTracker.removeListener("latest", latestBlockHandler);
516
+ listenersAreActive = false;
517
+ }
518
+ });
519
+ }
520
+
521
+ private async onLatestBlock(blockNumber: EthereumBlock) {
522
+ try {
523
+ await this.pendingTxTracker.updatePendingTxs();
524
+ } catch (error) {
525
+ log.error(error);
526
+ }
527
+
528
+ try {
529
+ await this.pendingTxTracker.resubmitPendingTxs(blockNumber);
530
+ } catch (error) {
531
+ log.error(error);
532
+ }
533
+ }
534
+
535
+ private async getCommonConfiguration(fromAddress: string) {
536
+ const { chainId, displayName } = this.getProviderConfig();
537
+ const supportsEIP1559 = await this.getEIP1559Compatibility(fromAddress);
538
+ const hardfork = supportsEIP1559 ? Hardfork.Paris : Hardfork.Berlin;
539
+ return Common.custom({
540
+ chainId: chainId === "loading" ? 0 : Number.parseInt(chainId, 16),
541
+ defaultHardfork: hardfork,
542
+ name: displayName,
543
+ networkId: chainId === "loading" ? 0 : Number.parseInt(chainId, 16),
544
+ });
545
+ }
546
+
547
+ private markNonceDuplicatesDropped(txId: string) {
548
+ const txMeta = this.getTransaction(txId);
549
+ const { nonce, from } = txMeta.transaction;
550
+ const sameNonceTxs = this.getTransactions({ searchCriteria: { from, nonce } });
551
+ if (!sameNonceTxs.length) return;
552
+ sameNonceTxs.forEach((tx) => {
553
+ if (tx.id === txId) return;
554
+ this.updateTransactionInState(txMeta, "transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce");
555
+ if (tx.status !== TransactionStatus.failed) this.setTxStatusDropped(tx.id);
556
+ });
557
+ }
558
+ }
@@ -0,0 +1,74 @@
1
+ import { addHexPrefix, stripHexPrefix } from "@ethereumjs/util";
2
+ import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
3
+ import { BN } from "bn.js";
4
+ import { cloneDeep } from "lodash";
5
+ import log from "loglevel";
6
+
7
+ import PollingBlockTracker from "../Block/PollingBlockTracker";
8
+ import { EthereumTransactionMeta, TransactionParams } from "../utils/interfaces";
9
+ export default class TransactionGasUtil {
10
+ provider: SafeEventEmitterProvider;
11
+
12
+ blockTracker: PollingBlockTracker;
13
+
14
+ constructor(provider: SafeEventEmitterProvider, blockTracker: PollingBlockTracker) {
15
+ this.provider = provider;
16
+ this.blockTracker = blockTracker;
17
+ }
18
+
19
+ public async analyzeGasUsage(txMeta: EthereumTransactionMeta) {
20
+ const block = await this.blockTracker.getLatestBlock();
21
+ // fallback to block gasLimit
22
+ const blockGasLimitBN = new BN(stripHexPrefix(block.gasLimit), 16);
23
+ const saferGasLimitBN = blockGasLimitBN.mul(new BN(19)).div(new BN(20));
24
+ let estimatedGasHex = addHexPrefix(saferGasLimitBN.toString("hex"));
25
+ let simulationFails: Record<string, unknown>;
26
+
27
+ try {
28
+ estimatedGasHex = await this.estimateTxGas(txMeta);
29
+ } catch (error) {
30
+ log.warn(error);
31
+ simulationFails = {
32
+ reason: (error as Error).message,
33
+ errorKey: (error as Error & { errorKey: string }).errorKey,
34
+ debug: { blockNumber: block.idempotencyKey, blockGasLimit: block.gasLimit },
35
+ };
36
+ }
37
+ return { blockGasLimit: block.gasLimit, estimatedGasHex, simulationFails };
38
+ }
39
+
40
+ /**
41
+ Adds a gas buffer with out exceeding the block gas limit
42
+ */
43
+ public addGasBuffer(initialGasLimitHex: string, blockGasLimitHex: string, multiplier = 1.5): string {
44
+ const initialGasLimitBn = new BN(stripHexPrefix(initialGasLimitHex), 16);
45
+ const blockGasLimitBn = new BN(stripHexPrefix(blockGasLimitHex), 16);
46
+ const upperGasLimitBn = blockGasLimitBn.muln(0.9);
47
+ const bufferedGasLimitBn = initialGasLimitBn.muln(multiplier);
48
+
49
+ // if initialGasLimit is above blockGasLimit, dont modify it
50
+ if (initialGasLimitBn.gt(upperGasLimitBn)) return addHexPrefix(initialGasLimitBn.toString("hex"));
51
+ // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
52
+ if (bufferedGasLimitBn.lt(upperGasLimitBn)) return addHexPrefix(bufferedGasLimitBn.toString("hex"));
53
+ // otherwise use blockGasLimit
54
+ return addHexPrefix(upperGasLimitBn.toString("hex"));
55
+ }
56
+
57
+ /**
58
+ Estimates the tx's gas usage
59
+ */
60
+ private async estimateTxGas(txMeta: EthereumTransactionMeta): Promise<string> {
61
+ const txParams = cloneDeep(txMeta.transaction);
62
+
63
+ // `eth_estimateGas` can fail if the user has insufficient balance for the
64
+ // value being sent, or for the gas cost. We don't want to check their
65
+ // balance here, we just want the gas estimate. The gas price is removed
66
+ // to skip those balance checks. We check balance elsewhere. We also delete
67
+ // maxFeePerGas and maxPriorityFeePerGas to support EIP-1559 txs.
68
+ delete txParams.gasPrice;
69
+ delete txParams.maxFeePerGas;
70
+ delete txParams.maxPriorityFeePerGas;
71
+
72
+ return this.provider.request<[TransactionParams], string>({ method: "eth_estimateGas", params: [txParams] });
73
+ }
74
+ }
@@ -0,0 +1,41 @@
1
+ import jsonDiffer, { Operation } from "fast-json-patch";
2
+ import { cloneDeep } from "lodash";
3
+
4
+ import { EthereumTransactionMeta } from "../utils/interfaces";
5
+
6
+ /**
7
+ Generates an array of history objects sense the previous state.
8
+ The object has the keys
9
+ op (the operation performed),
10
+ path (the key and if a nested object then each key will be seperated with a `/`)
11
+ value
12
+ with the first entry having the note and a timestamp when the change took place
13
+ */
14
+ function generateHistoryEntry(previousState: Record<string, unknown>, newState: Record<string, unknown>, note?: string): Record<string, unknown>[] {
15
+ const entry = jsonDiffer.compare(previousState, newState) as unknown as Record<string, unknown>[];
16
+ // Add a note to the first op, since it breaks if we append it to the entry
17
+ if (entry[0]) {
18
+ if (note) {
19
+ entry[0].note = note;
20
+ }
21
+
22
+ entry[0].timestamp = Date.now();
23
+ }
24
+ return entry;
25
+ }
26
+
27
+ /**
28
+ Recovers previous txMeta state obj
29
+ */
30
+ function replayHistory(_shortHistory: Record<string, unknown>[]): Record<string, unknown> {
31
+ const shortHistory = cloneDeep(_shortHistory);
32
+ return shortHistory.reduce((val: Record<string, unknown>, entry: unknown) => jsonDiffer.applyPatch(val, entry as Operation[]).newDocument);
33
+ }
34
+
35
+ function snapshotFromTxMeta(txMeta: EthereumTransactionMeta): EthereumTransactionMeta {
36
+ const shallow = { ...txMeta };
37
+ delete shallow.history;
38
+ return cloneDeep(shallow);
39
+ }
40
+
41
+ export { generateHistoryEntry, replayHistory, snapshotFromTxMeta };