@metamask/transaction-controller 1.0.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.
@@ -0,0 +1,927 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.TransactionController = exports.SPEED_UP_RATE = exports.CANCEL_RATE = exports.WalletDevice = exports.TransactionStatus = void 0;
16
+ const events_1 = require("events");
17
+ const ethereumjs_util_1 = require("ethereumjs-util");
18
+ const eth_rpc_errors_1 = require("eth-rpc-errors");
19
+ const eth_method_registry_1 = __importDefault(require("eth-method-registry"));
20
+ const eth_query_1 = __importDefault(require("eth-query"));
21
+ const common_1 = __importDefault(require("@ethereumjs/common"));
22
+ const tx_1 = require("@ethereumjs/tx");
23
+ const uuid_1 = require("uuid");
24
+ const async_mutex_1 = require("async-mutex");
25
+ const base_controller_1 = require("@metamask/base-controller");
26
+ const controller_utils_1 = require("@metamask/controller-utils");
27
+ const utils_1 = require("./utils");
28
+ const HARDFORK = 'london';
29
+ /**
30
+ * The status of the transaction. Each status represents the state of the transaction internally
31
+ * in the wallet. Some of these correspond with the state of the transaction on the network, but
32
+ * some are wallet-specific.
33
+ */
34
+ var TransactionStatus;
35
+ (function (TransactionStatus) {
36
+ TransactionStatus["approved"] = "approved";
37
+ TransactionStatus["cancelled"] = "cancelled";
38
+ TransactionStatus["confirmed"] = "confirmed";
39
+ TransactionStatus["failed"] = "failed";
40
+ TransactionStatus["rejected"] = "rejected";
41
+ TransactionStatus["signed"] = "signed";
42
+ TransactionStatus["submitted"] = "submitted";
43
+ TransactionStatus["unapproved"] = "unapproved";
44
+ })(TransactionStatus = exports.TransactionStatus || (exports.TransactionStatus = {}));
45
+ /**
46
+ * Options for wallet device.
47
+ */
48
+ var WalletDevice;
49
+ (function (WalletDevice) {
50
+ WalletDevice["MM_MOBILE"] = "metamask_mobile";
51
+ WalletDevice["MM_EXTENSION"] = "metamask_extension";
52
+ WalletDevice["OTHER"] = "other_device";
53
+ })(WalletDevice = exports.WalletDevice || (exports.WalletDevice = {}));
54
+ /**
55
+ * Multiplier used to determine a transaction's increased gas fee during cancellation
56
+ */
57
+ exports.CANCEL_RATE = 1.5;
58
+ /**
59
+ * Multiplier used to determine a transaction's increased gas fee during speed up
60
+ */
61
+ exports.SPEED_UP_RATE = 1.1;
62
+ /**
63
+ * Controller responsible for submitting and managing transactions.
64
+ */
65
+ class TransactionController extends base_controller_1.BaseController {
66
+ /**
67
+ * Creates a TransactionController instance.
68
+ *
69
+ * @param options - The controller options.
70
+ * @param options.getNetworkState - Gets the state of the network controller.
71
+ * @param options.onNetworkStateChange - Allows subscribing to network controller state changes.
72
+ * @param options.getProvider - Returns a provider for the current network.
73
+ * @param config - Initial options used to configure this controller.
74
+ * @param state - Initial state to set on this controller.
75
+ */
76
+ constructor({ getNetworkState, onNetworkStateChange, getProvider, }, config, state) {
77
+ super(config, state);
78
+ this.mutex = new async_mutex_1.Mutex();
79
+ this.normalizeTokenTx = (txMeta, currentNetworkID, currentChainId) => {
80
+ const time = parseInt(txMeta.timeStamp, 10) * 1000;
81
+ const { to, from, gas, gasPrice, gasUsed, hash, contractAddress, tokenDecimal, tokenSymbol, value, } = txMeta;
82
+ return {
83
+ id: (0, uuid_1.v1)({ msecs: time }),
84
+ isTransfer: true,
85
+ networkID: currentNetworkID,
86
+ chainId: currentChainId,
87
+ status: TransactionStatus.confirmed,
88
+ time,
89
+ transaction: {
90
+ chainId: 1,
91
+ from,
92
+ gas,
93
+ gasPrice,
94
+ gasUsed,
95
+ to,
96
+ value,
97
+ },
98
+ transactionHash: hash,
99
+ transferInformation: {
100
+ contractAddress,
101
+ decimals: Number(tokenDecimal),
102
+ symbol: tokenSymbol,
103
+ },
104
+ verifiedOnBlockchain: false,
105
+ };
106
+ };
107
+ /**
108
+ * EventEmitter instance used to listen to specific transactional events
109
+ */
110
+ this.hub = new events_1.EventEmitter();
111
+ /**
112
+ * Name of this controller used during composition
113
+ */
114
+ this.name = 'TransactionController';
115
+ this.defaultConfig = {
116
+ interval: 15000,
117
+ txHistoryLimit: 40,
118
+ };
119
+ this.defaultState = {
120
+ methodData: {},
121
+ transactions: [],
122
+ };
123
+ this.initialize();
124
+ const provider = getProvider();
125
+ this.getNetworkState = getNetworkState;
126
+ this.ethQuery = new eth_query_1.default(provider);
127
+ this.registry = new eth_method_registry_1.default({ provider });
128
+ onNetworkStateChange(() => {
129
+ const newProvider = getProvider();
130
+ this.ethQuery = new eth_query_1.default(newProvider);
131
+ this.registry = new eth_method_registry_1.default({ provider: newProvider });
132
+ });
133
+ this.poll();
134
+ }
135
+ failTransaction(transactionMeta, error) {
136
+ const newTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { error, status: TransactionStatus.failed });
137
+ this.updateTransaction(newTransactionMeta);
138
+ this.hub.emit(`${transactionMeta.id}:finished`, newTransactionMeta);
139
+ }
140
+ registryLookup(fourBytePrefix) {
141
+ return __awaiter(this, void 0, void 0, function* () {
142
+ const registryMethod = yield this.registry.lookup(fourBytePrefix);
143
+ const parsedRegistryMethod = this.registry.parse(registryMethod);
144
+ return { registryMethod, parsedRegistryMethod };
145
+ });
146
+ }
147
+ /**
148
+ * Normalizes the transaction information from etherscan
149
+ * to be compatible with the TransactionMeta interface.
150
+ *
151
+ * @param txMeta - The transaction.
152
+ * @param currentNetworkID - The current network ID.
153
+ * @param currentChainId - The current chain ID.
154
+ * @returns The normalized transaction.
155
+ */
156
+ normalizeTx(txMeta, currentNetworkID, currentChainId) {
157
+ const time = parseInt(txMeta.timeStamp, 10) * 1000;
158
+ const normalizedTransactionBase = {
159
+ blockNumber: txMeta.blockNumber,
160
+ id: (0, uuid_1.v1)({ msecs: time }),
161
+ networkID: currentNetworkID,
162
+ chainId: currentChainId,
163
+ time,
164
+ transaction: {
165
+ data: txMeta.input,
166
+ from: txMeta.from,
167
+ gas: (0, controller_utils_1.BNToHex)(new ethereumjs_util_1.BN(txMeta.gas)),
168
+ gasPrice: (0, controller_utils_1.BNToHex)(new ethereumjs_util_1.BN(txMeta.gasPrice)),
169
+ gasUsed: (0, controller_utils_1.BNToHex)(new ethereumjs_util_1.BN(txMeta.gasUsed)),
170
+ nonce: (0, controller_utils_1.BNToHex)(new ethereumjs_util_1.BN(txMeta.nonce)),
171
+ to: txMeta.to,
172
+ value: (0, controller_utils_1.BNToHex)(new ethereumjs_util_1.BN(txMeta.value)),
173
+ },
174
+ transactionHash: txMeta.hash,
175
+ verifiedOnBlockchain: false,
176
+ };
177
+ /* istanbul ignore else */
178
+ if (txMeta.isError === '0') {
179
+ return Object.assign(Object.assign({}, normalizedTransactionBase), { status: TransactionStatus.confirmed });
180
+ }
181
+ /* istanbul ignore next */
182
+ return Object.assign(Object.assign({}, normalizedTransactionBase), { error: new Error('Transaction failed'), status: TransactionStatus.failed });
183
+ }
184
+ /**
185
+ * Starts a new polling interval.
186
+ *
187
+ * @param interval - The polling interval used to fetch new transaction statuses.
188
+ */
189
+ poll(interval) {
190
+ return __awaiter(this, void 0, void 0, function* () {
191
+ interval && this.configure({ interval }, false, false);
192
+ this.handle && clearTimeout(this.handle);
193
+ yield (0, controller_utils_1.safelyExecute)(() => this.queryTransactionStatuses());
194
+ this.handle = setTimeout(() => {
195
+ this.poll(this.config.interval);
196
+ }, this.config.interval);
197
+ });
198
+ }
199
+ /**
200
+ * Handle new method data request.
201
+ *
202
+ * @param fourBytePrefix - The method prefix.
203
+ * @returns The method data object corresponding to the given signature prefix.
204
+ */
205
+ handleMethodData(fourBytePrefix) {
206
+ return __awaiter(this, void 0, void 0, function* () {
207
+ const releaseLock = yield this.mutex.acquire();
208
+ try {
209
+ const { methodData } = this.state;
210
+ const knownMethod = Object.keys(methodData).find((knownFourBytePrefix) => fourBytePrefix === knownFourBytePrefix);
211
+ if (knownMethod) {
212
+ return methodData[fourBytePrefix];
213
+ }
214
+ const registry = yield this.registryLookup(fourBytePrefix);
215
+ this.update({
216
+ methodData: Object.assign(Object.assign({}, methodData), { [fourBytePrefix]: registry }),
217
+ });
218
+ return registry;
219
+ }
220
+ finally {
221
+ releaseLock();
222
+ }
223
+ });
224
+ }
225
+ /**
226
+ * Add a new unapproved transaction to state. Parameters will be validated, a
227
+ * unique transaction id will be generated, and gas and gasPrice will be calculated
228
+ * if not provided. If A `<tx.id>:unapproved` hub event will be emitted once added.
229
+ *
230
+ * @param transaction - The transaction object to add.
231
+ * @param origin - The domain origin to append to the generated TransactionMeta.
232
+ * @param deviceConfirmedOn - An enum to indicate what device the transaction was confirmed to append to the generated TransactionMeta.
233
+ * @returns Object containing a promise resolving to the transaction hash if approved.
234
+ */
235
+ addTransaction(transaction, origin, deviceConfirmedOn) {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ const { provider, network } = this.getNetworkState();
238
+ const { transactions } = this.state;
239
+ transaction = (0, utils_1.normalizeTransaction)(transaction);
240
+ (0, utils_1.validateTransaction)(transaction);
241
+ const transactionMeta = {
242
+ id: (0, uuid_1.v1)(),
243
+ networkID: network,
244
+ chainId: provider.chainId,
245
+ origin,
246
+ status: TransactionStatus.unapproved,
247
+ time: Date.now(),
248
+ transaction,
249
+ deviceConfirmedOn,
250
+ verifiedOnBlockchain: false,
251
+ };
252
+ try {
253
+ const { gas, estimateGasError } = yield this.estimateGas(transaction);
254
+ transaction.gas = gas;
255
+ transaction.estimateGasError = estimateGasError;
256
+ }
257
+ catch (error) {
258
+ this.failTransaction(transactionMeta, error);
259
+ return Promise.reject(error);
260
+ }
261
+ const result = new Promise((resolve, reject) => {
262
+ this.hub.once(`${transactionMeta.id}:finished`, (meta) => {
263
+ switch (meta.status) {
264
+ case TransactionStatus.submitted:
265
+ return resolve(meta.transactionHash);
266
+ case TransactionStatus.rejected:
267
+ return reject(eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the transaction'));
268
+ case TransactionStatus.cancelled:
269
+ return reject(eth_rpc_errors_1.ethErrors.rpc.internal('User cancelled the transaction'));
270
+ case TransactionStatus.failed:
271
+ return reject(eth_rpc_errors_1.ethErrors.rpc.internal(meta.error.message));
272
+ /* istanbul ignore next */
273
+ default:
274
+ return reject(eth_rpc_errors_1.ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(meta)}`));
275
+ }
276
+ });
277
+ });
278
+ transactions.push(transactionMeta);
279
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
280
+ this.hub.emit(`unapprovedTransaction`, transactionMeta);
281
+ return { result, transactionMeta };
282
+ });
283
+ }
284
+ prepareUnsignedEthTx(txParams) {
285
+ return tx_1.TransactionFactory.fromTxData(txParams, {
286
+ common: this.getCommonConfiguration(),
287
+ freeze: false,
288
+ });
289
+ }
290
+ /**
291
+ * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for
292
+ * specifying which chain, network, hardfork and EIPs to support for
293
+ * a transaction. By referencing this configuration, and analyzing the fields
294
+ * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718
295
+ * transaction type to use.
296
+ *
297
+ * @returns {Common} common configuration object
298
+ */
299
+ getCommonConfiguration() {
300
+ const { network: networkId, provider: { type: chain, chainId, nickname: name }, } = this.getNetworkState();
301
+ if (chain !== controller_utils_1.RPC) {
302
+ return new common_1.default({ chain, hardfork: HARDFORK });
303
+ }
304
+ const customChainParams = {
305
+ name,
306
+ chainId: parseInt(chainId, undefined),
307
+ networkId: parseInt(networkId, undefined),
308
+ };
309
+ return common_1.default.forCustomChain(controller_utils_1.MAINNET, customChainParams, HARDFORK);
310
+ }
311
+ /**
312
+ * Approves a transaction and updates it's status in state. If this is not a
313
+ * retry transaction, a nonce will be generated. The transaction is signed
314
+ * using the sign configuration property, then published to the blockchain.
315
+ * A `<tx.id>:finished` hub event is fired after success or failure.
316
+ *
317
+ * @param transactionID - The ID of the transaction to approve.
318
+ */
319
+ approveTransaction(transactionID) {
320
+ return __awaiter(this, void 0, void 0, function* () {
321
+ const { transactions } = this.state;
322
+ const releaseLock = yield this.mutex.acquire();
323
+ const { provider } = this.getNetworkState();
324
+ const { chainId: currentChainId } = provider;
325
+ const index = transactions.findIndex(({ id }) => transactionID === id);
326
+ const transactionMeta = transactions[index];
327
+ const { nonce } = transactionMeta.transaction;
328
+ try {
329
+ const { from } = transactionMeta.transaction;
330
+ if (!this.sign) {
331
+ releaseLock();
332
+ this.failTransaction(transactionMeta, new Error('No sign method defined.'));
333
+ return;
334
+ }
335
+ else if (!currentChainId) {
336
+ releaseLock();
337
+ this.failTransaction(transactionMeta, new Error('No chainId defined.'));
338
+ return;
339
+ }
340
+ const chainId = parseInt(currentChainId, undefined);
341
+ const { approved: status } = TransactionStatus;
342
+ const txNonce = nonce ||
343
+ (yield (0, controller_utils_1.query)(this.ethQuery, 'getTransactionCount', [from, 'pending']));
344
+ transactionMeta.status = status;
345
+ transactionMeta.transaction.nonce = txNonce;
346
+ transactionMeta.transaction.chainId = chainId;
347
+ const baseTxParams = Object.assign(Object.assign({}, transactionMeta.transaction), { gasLimit: transactionMeta.transaction.gas, chainId, nonce: txNonce, status });
348
+ const isEIP1559 = (0, utils_1.isEIP1559Transaction)(transactionMeta.transaction);
349
+ const txParams = isEIP1559
350
+ ? Object.assign(Object.assign({}, baseTxParams), { maxFeePerGas: transactionMeta.transaction.maxFeePerGas, maxPriorityFeePerGas: transactionMeta.transaction.maxPriorityFeePerGas, estimatedBaseFee: transactionMeta.transaction.estimatedBaseFee,
351
+ // specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set
352
+ type: 2 }) : baseTxParams;
353
+ // delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set
354
+ if (isEIP1559) {
355
+ delete txParams.gasPrice;
356
+ }
357
+ const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
358
+ const signedTx = yield this.sign(unsignedEthTx, from);
359
+ transactionMeta.status = TransactionStatus.signed;
360
+ this.updateTransaction(transactionMeta);
361
+ const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
362
+ transactionMeta.rawTransaction = rawTransaction;
363
+ this.updateTransaction(transactionMeta);
364
+ const transactionHash = yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [
365
+ rawTransaction,
366
+ ]);
367
+ transactionMeta.transactionHash = transactionHash;
368
+ transactionMeta.status = TransactionStatus.submitted;
369
+ this.updateTransaction(transactionMeta);
370
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
371
+ }
372
+ catch (error) {
373
+ this.failTransaction(transactionMeta, error);
374
+ }
375
+ finally {
376
+ releaseLock();
377
+ }
378
+ });
379
+ }
380
+ /**
381
+ * Cancels a transaction based on its ID by setting its status to "rejected"
382
+ * and emitting a `<tx.id>:finished` hub event.
383
+ *
384
+ * @param transactionID - The ID of the transaction to cancel.
385
+ */
386
+ cancelTransaction(transactionID) {
387
+ const transactionMeta = this.state.transactions.find(({ id }) => id === transactionID);
388
+ if (!transactionMeta) {
389
+ return;
390
+ }
391
+ transactionMeta.status = TransactionStatus.rejected;
392
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
393
+ const transactions = this.state.transactions.filter(({ id }) => id !== transactionID);
394
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
395
+ }
396
+ /**
397
+ * Attempts to cancel a transaction based on its ID by setting its status to "rejected"
398
+ * and emitting a `<tx.id>:finished` hub event.
399
+ *
400
+ * @param transactionID - The ID of the transaction to cancel.
401
+ * @param gasValues - The gas values to use for the cancellation transation.
402
+ */
403
+ stopTransaction(transactionID, gasValues) {
404
+ var _a, _b;
405
+ return __awaiter(this, void 0, void 0, function* () {
406
+ if (gasValues) {
407
+ (0, utils_1.validateGasValues)(gasValues);
408
+ }
409
+ const transactionMeta = this.state.transactions.find(({ id }) => id === transactionID);
410
+ if (!transactionMeta) {
411
+ return;
412
+ }
413
+ if (!this.sign) {
414
+ throw new Error('No sign method defined.');
415
+ }
416
+ // gasPrice (legacy non EIP1559)
417
+ const minGasPrice = (0, utils_1.getIncreasedPriceFromExisting)(transactionMeta.transaction.gasPrice, exports.CANCEL_RATE);
418
+ const gasPriceFromValues = (0, utils_1.isGasPriceValue)(gasValues) && gasValues.gasPrice;
419
+ const newGasPrice = (gasPriceFromValues &&
420
+ (0, utils_1.validateMinimumIncrease)(gasPriceFromValues, minGasPrice)) ||
421
+ minGasPrice;
422
+ // maxFeePerGas (EIP1559)
423
+ const existingMaxFeePerGas = (_a = transactionMeta.transaction) === null || _a === void 0 ? void 0 : _a.maxFeePerGas;
424
+ const minMaxFeePerGas = (0, utils_1.getIncreasedPriceFromExisting)(existingMaxFeePerGas, exports.CANCEL_RATE);
425
+ const maxFeePerGasValues = (0, utils_1.isFeeMarketEIP1559Values)(gasValues) && gasValues.maxFeePerGas;
426
+ const newMaxFeePerGas = (maxFeePerGasValues &&
427
+ (0, utils_1.validateMinimumIncrease)(maxFeePerGasValues, minMaxFeePerGas)) ||
428
+ (existingMaxFeePerGas && minMaxFeePerGas);
429
+ // maxPriorityFeePerGas (EIP1559)
430
+ const existingMaxPriorityFeePerGas = (_b = transactionMeta.transaction) === null || _b === void 0 ? void 0 : _b.maxPriorityFeePerGas;
431
+ const minMaxPriorityFeePerGas = (0, utils_1.getIncreasedPriceFromExisting)(existingMaxPriorityFeePerGas, exports.CANCEL_RATE);
432
+ const maxPriorityFeePerGasValues = (0, utils_1.isFeeMarketEIP1559Values)(gasValues) && gasValues.maxPriorityFeePerGas;
433
+ const newMaxPriorityFeePerGas = (maxPriorityFeePerGasValues &&
434
+ (0, utils_1.validateMinimumIncrease)(maxPriorityFeePerGasValues, minMaxPriorityFeePerGas)) ||
435
+ (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);
436
+ const txParams = newMaxFeePerGas && newMaxPriorityFeePerGas
437
+ ? {
438
+ from: transactionMeta.transaction.from,
439
+ gasLimit: transactionMeta.transaction.gas,
440
+ maxFeePerGas: newMaxFeePerGas,
441
+ maxPriorityFeePerGas: newMaxPriorityFeePerGas,
442
+ type: 2,
443
+ nonce: transactionMeta.transaction.nonce,
444
+ to: transactionMeta.transaction.from,
445
+ value: '0x0',
446
+ }
447
+ : {
448
+ from: transactionMeta.transaction.from,
449
+ gasLimit: transactionMeta.transaction.gas,
450
+ gasPrice: newGasPrice,
451
+ nonce: transactionMeta.transaction.nonce,
452
+ to: transactionMeta.transaction.from,
453
+ value: '0x0',
454
+ };
455
+ const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
456
+ const signedTx = yield this.sign(unsignedEthTx, transactionMeta.transaction.from);
457
+ const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
458
+ yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTransaction]);
459
+ transactionMeta.status = TransactionStatus.cancelled;
460
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
461
+ });
462
+ }
463
+ /**
464
+ * Attempts to speed up a transaction increasing transaction gasPrice by ten percent.
465
+ *
466
+ * @param transactionID - The ID of the transaction to speed up.
467
+ * @param gasValues - The gas values to use for the speed up transation.
468
+ */
469
+ speedUpTransaction(transactionID, gasValues) {
470
+ var _a, _b;
471
+ return __awaiter(this, void 0, void 0, function* () {
472
+ if (gasValues) {
473
+ (0, utils_1.validateGasValues)(gasValues);
474
+ }
475
+ const transactionMeta = this.state.transactions.find(({ id }) => id === transactionID);
476
+ /* istanbul ignore next */
477
+ if (!transactionMeta) {
478
+ return;
479
+ }
480
+ /* istanbul ignore next */
481
+ if (!this.sign) {
482
+ throw new Error('No sign method defined.');
483
+ }
484
+ const { transactions } = this.state;
485
+ // gasPrice (legacy non EIP1559)
486
+ const minGasPrice = (0, utils_1.getIncreasedPriceFromExisting)(transactionMeta.transaction.gasPrice, exports.SPEED_UP_RATE);
487
+ const gasPriceFromValues = (0, utils_1.isGasPriceValue)(gasValues) && gasValues.gasPrice;
488
+ const newGasPrice = (gasPriceFromValues &&
489
+ (0, utils_1.validateMinimumIncrease)(gasPriceFromValues, minGasPrice)) ||
490
+ minGasPrice;
491
+ // maxFeePerGas (EIP1559)
492
+ const existingMaxFeePerGas = (_a = transactionMeta.transaction) === null || _a === void 0 ? void 0 : _a.maxFeePerGas;
493
+ const minMaxFeePerGas = (0, utils_1.getIncreasedPriceFromExisting)(existingMaxFeePerGas, exports.SPEED_UP_RATE);
494
+ const maxFeePerGasValues = (0, utils_1.isFeeMarketEIP1559Values)(gasValues) && gasValues.maxFeePerGas;
495
+ const newMaxFeePerGas = (maxFeePerGasValues &&
496
+ (0, utils_1.validateMinimumIncrease)(maxFeePerGasValues, minMaxFeePerGas)) ||
497
+ (existingMaxFeePerGas && minMaxFeePerGas);
498
+ // maxPriorityFeePerGas (EIP1559)
499
+ const existingMaxPriorityFeePerGas = (_b = transactionMeta.transaction) === null || _b === void 0 ? void 0 : _b.maxPriorityFeePerGas;
500
+ const minMaxPriorityFeePerGas = (0, utils_1.getIncreasedPriceFromExisting)(existingMaxPriorityFeePerGas, exports.SPEED_UP_RATE);
501
+ const maxPriorityFeePerGasValues = (0, utils_1.isFeeMarketEIP1559Values)(gasValues) && gasValues.maxPriorityFeePerGas;
502
+ const newMaxPriorityFeePerGas = (maxPriorityFeePerGasValues &&
503
+ (0, utils_1.validateMinimumIncrease)(maxPriorityFeePerGasValues, minMaxPriorityFeePerGas)) ||
504
+ (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);
505
+ const txParams = newMaxFeePerGas && newMaxPriorityFeePerGas
506
+ ? Object.assign(Object.assign({}, transactionMeta.transaction), { gasLimit: transactionMeta.transaction.gas, maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas, type: 2 }) : Object.assign(Object.assign({}, transactionMeta.transaction), { gasLimit: transactionMeta.transaction.gas, gasPrice: newGasPrice });
507
+ const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
508
+ const signedTx = yield this.sign(unsignedEthTx, transactionMeta.transaction.from);
509
+ const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
510
+ const transactionHash = yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [
511
+ rawTransaction,
512
+ ]);
513
+ const baseTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { id: (0, uuid_1.v1)(), time: Date.now(), transactionHash });
514
+ const newTransactionMeta = newMaxFeePerGas && newMaxPriorityFeePerGas
515
+ ? Object.assign(Object.assign({}, baseTransactionMeta), { transaction: Object.assign(Object.assign({}, transactionMeta.transaction), { maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas }) }) : Object.assign(Object.assign({}, baseTransactionMeta), { transaction: Object.assign(Object.assign({}, transactionMeta.transaction), { gasPrice: newGasPrice }) });
516
+ transactions.push(newTransactionMeta);
517
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
518
+ this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);
519
+ });
520
+ }
521
+ /**
522
+ * Estimates required gas for a given transaction.
523
+ *
524
+ * @param transaction - The transaction to estimate gas for.
525
+ * @returns The gas and gas price.
526
+ */
527
+ estimateGas(transaction) {
528
+ return __awaiter(this, void 0, void 0, function* () {
529
+ const estimatedTransaction = Object.assign({}, transaction);
530
+ const { gas, gasPrice: providedGasPrice, to, value, data, } = estimatedTransaction;
531
+ const gasPrice = typeof providedGasPrice === 'undefined'
532
+ ? yield (0, controller_utils_1.query)(this.ethQuery, 'gasPrice')
533
+ : providedGasPrice;
534
+ const { isCustomNetwork } = this.getNetworkState();
535
+ // 1. If gas is already defined on the transaction, use it
536
+ if (typeof gas !== 'undefined') {
537
+ return { gas, gasPrice };
538
+ }
539
+ const { gasLimit } = yield (0, controller_utils_1.query)(this.ethQuery, 'getBlockByNumber', [
540
+ 'latest',
541
+ false,
542
+ ]);
543
+ // 2. If to is not defined or this is not a contract address, and there is no data use 0x5208 / 21000.
544
+ // If the newtwork is a custom network then bypass this check and fetch 'estimateGas'.
545
+ /* istanbul ignore next */
546
+ const code = to ? yield (0, controller_utils_1.query)(this.ethQuery, 'getCode', [to]) : undefined;
547
+ /* istanbul ignore next */
548
+ if (!isCustomNetwork &&
549
+ (!to || (to && !data && (!code || code === '0x')))) {
550
+ return { gas: '0x5208', gasPrice };
551
+ }
552
+ // if data, should be hex string format
553
+ estimatedTransaction.data = !data
554
+ ? data
555
+ : /* istanbul ignore next */ (0, ethereumjs_util_1.addHexPrefix)(data);
556
+ // 3. If this is a contract address, safely estimate gas using RPC
557
+ estimatedTransaction.value =
558
+ typeof value === 'undefined' ? '0x0' : /* istanbul ignore next */ value;
559
+ const gasLimitBN = (0, controller_utils_1.hexToBN)(gasLimit);
560
+ estimatedTransaction.gas = (0, controller_utils_1.BNToHex)((0, controller_utils_1.fractionBN)(gasLimitBN, 19, 20));
561
+ let gasHex;
562
+ let estimateGasError;
563
+ try {
564
+ gasHex = yield (0, controller_utils_1.query)(this.ethQuery, 'estimateGas', [
565
+ estimatedTransaction,
566
+ ]);
567
+ }
568
+ catch (error) {
569
+ estimateGasError = utils_1.ESTIMATE_GAS_ERROR;
570
+ }
571
+ // 4. Pad estimated gas without exceeding the most recent block gasLimit. If the network is a
572
+ // a custom network then return the eth_estimateGas value.
573
+ const gasBN = (0, controller_utils_1.hexToBN)(gasHex);
574
+ const maxGasBN = gasLimitBN.muln(0.9);
575
+ const paddedGasBN = gasBN.muln(1.5);
576
+ /* istanbul ignore next */
577
+ if (gasBN.gt(maxGasBN) || isCustomNetwork) {
578
+ return { gas: (0, ethereumjs_util_1.addHexPrefix)(gasHex), gasPrice, estimateGasError };
579
+ }
580
+ /* istanbul ignore next */
581
+ if (paddedGasBN.lt(maxGasBN)) {
582
+ return {
583
+ gas: (0, ethereumjs_util_1.addHexPrefix)((0, controller_utils_1.BNToHex)(paddedGasBN)),
584
+ gasPrice,
585
+ estimateGasError,
586
+ };
587
+ }
588
+ return { gas: (0, ethereumjs_util_1.addHexPrefix)((0, controller_utils_1.BNToHex)(maxGasBN)), gasPrice };
589
+ });
590
+ }
591
+ /**
592
+ * Check the status of submitted transactions on the network to determine whether they have
593
+ * been included in a block. Any that have been included in a block are marked as confirmed.
594
+ */
595
+ queryTransactionStatuses() {
596
+ return __awaiter(this, void 0, void 0, function* () {
597
+ const { transactions } = this.state;
598
+ const { provider, network: currentNetworkID } = this.getNetworkState();
599
+ const { chainId: currentChainId } = provider;
600
+ let gotUpdates = false;
601
+ yield (0, controller_utils_1.safelyExecute)(() => Promise.all(transactions.map((meta, index) => __awaiter(this, void 0, void 0, function* () {
602
+ // Using fallback to networkID only when there is no chainId present.
603
+ // Should be removed when networkID is completely removed.
604
+ const txBelongsToCurrentChain = meta.chainId === currentChainId ||
605
+ (!meta.chainId && meta.networkID === currentNetworkID);
606
+ if (!meta.verifiedOnBlockchain && txBelongsToCurrentChain) {
607
+ const [reconciledTx, updateRequired] = yield this.blockchainTransactionStateReconciler(meta);
608
+ if (updateRequired) {
609
+ transactions[index] = reconciledTx;
610
+ gotUpdates = updateRequired;
611
+ }
612
+ }
613
+ }))));
614
+ /* istanbul ignore else */
615
+ if (gotUpdates) {
616
+ this.update({
617
+ transactions: this.trimTransactionsForState(transactions),
618
+ });
619
+ }
620
+ });
621
+ }
622
+ /**
623
+ * Updates an existing transaction in state.
624
+ *
625
+ * @param transactionMeta - The new transaction to store in state.
626
+ */
627
+ updateTransaction(transactionMeta) {
628
+ const { transactions } = this.state;
629
+ transactionMeta.transaction = (0, utils_1.normalizeTransaction)(transactionMeta.transaction);
630
+ (0, utils_1.validateTransaction)(transactionMeta.transaction);
631
+ const index = transactions.findIndex(({ id }) => transactionMeta.id === id);
632
+ transactions[index] = transactionMeta;
633
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
634
+ }
635
+ /**
636
+ * Removes all transactions from state, optionally based on the current network.
637
+ *
638
+ * @param ignoreNetwork - Determines whether to wipe all transactions, or just those on the
639
+ * current network. If `true`, all transactions are wiped.
640
+ */
641
+ wipeTransactions(ignoreNetwork) {
642
+ /* istanbul ignore next */
643
+ if (ignoreNetwork) {
644
+ this.update({ transactions: [] });
645
+ return;
646
+ }
647
+ const { provider, network: currentNetworkID } = this.getNetworkState();
648
+ const { chainId: currentChainId } = provider;
649
+ const newTransactions = this.state.transactions.filter(({ networkID, chainId }) => {
650
+ // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
651
+ const isCurrentNetwork = chainId === currentChainId ||
652
+ (!chainId && networkID === currentNetworkID);
653
+ return !isCurrentNetwork;
654
+ });
655
+ this.update({
656
+ transactions: this.trimTransactionsForState(newTransactions),
657
+ });
658
+ }
659
+ /**
660
+ * Get transactions from Etherscan for the given address. By default all transactions are
661
+ * returned, but the `fromBlock` option can be given to filter just for transactions from a
662
+ * specific block onward.
663
+ *
664
+ * @param address - The address to fetch the transactions for.
665
+ * @param opt - Object containing optional data, fromBlock and Etherscan API key.
666
+ * @returns The block number of the latest incoming transaction.
667
+ */
668
+ fetchAll(address, opt) {
669
+ return __awaiter(this, void 0, void 0, function* () {
670
+ const { provider, network: currentNetworkID } = this.getNetworkState();
671
+ const { chainId: currentChainId, type: networkType } = provider;
672
+ const { transactions } = this.state;
673
+ const supportedNetworkIds = ['1', '3', '4', '42'];
674
+ /* istanbul ignore next */
675
+ if (supportedNetworkIds.indexOf(currentNetworkID) === -1) {
676
+ return undefined;
677
+ }
678
+ const [etherscanTxResponse, etherscanTokenResponse] = yield (0, utils_1.handleTransactionFetch)(networkType, address, this.config.txHistoryLimit, opt);
679
+ const normalizedTxs = etherscanTxResponse.result.map((tx) => this.normalizeTx(tx, currentNetworkID, currentChainId));
680
+ const normalizedTokenTxs = etherscanTokenResponse.result.map((tx) => this.normalizeTokenTx(tx, currentNetworkID, currentChainId));
681
+ const [updateRequired, allTxs] = this.etherscanTransactionStateReconciler([...normalizedTxs, ...normalizedTokenTxs], transactions);
682
+ allTxs.sort((a, b) => (a.time < b.time ? -1 : 1));
683
+ let latestIncomingTxBlockNumber;
684
+ allTxs.forEach((tx) => __awaiter(this, void 0, void 0, function* () {
685
+ /* istanbul ignore next */
686
+ if (
687
+ // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
688
+ (tx.chainId === currentChainId ||
689
+ (!tx.chainId && tx.networkID === currentNetworkID)) &&
690
+ tx.transaction.to &&
691
+ tx.transaction.to.toLowerCase() === address.toLowerCase()) {
692
+ if (tx.blockNumber &&
693
+ (!latestIncomingTxBlockNumber ||
694
+ parseInt(latestIncomingTxBlockNumber, 10) <
695
+ parseInt(tx.blockNumber, 10))) {
696
+ latestIncomingTxBlockNumber = tx.blockNumber;
697
+ }
698
+ }
699
+ /* istanbul ignore else */
700
+ if (tx.toSmartContract === undefined) {
701
+ // If not `to` is a contract deploy, if not `data` is send eth
702
+ if (tx.transaction.to &&
703
+ (!tx.transaction.data || tx.transaction.data !== '0x')) {
704
+ const code = yield (0, controller_utils_1.query)(this.ethQuery, 'getCode', [
705
+ tx.transaction.to,
706
+ ]);
707
+ tx.toSmartContract = (0, controller_utils_1.isSmartContractCode)(code);
708
+ }
709
+ else {
710
+ tx.toSmartContract = false;
711
+ }
712
+ }
713
+ }));
714
+ // Update state only if new transactions were fetched or
715
+ // the status or gas data of a transaction has changed
716
+ if (updateRequired) {
717
+ this.update({ transactions: this.trimTransactionsForState(allTxs) });
718
+ }
719
+ return latestIncomingTxBlockNumber;
720
+ });
721
+ }
722
+ /**
723
+ * Trim the amount of transactions that are set on the state. Checks
724
+ * if the length of the tx history is longer then desired persistence
725
+ * limit and then if it is removes the oldest confirmed or rejected tx.
726
+ * Pending or unapproved transactions will not be removed by this
727
+ * operation. For safety of presenting a fully functional transaction UI
728
+ * representation, this function will not break apart transactions with the
729
+ * same nonce, created on the same day, per network. Not accounting for transactions of the same
730
+ * nonce, same day and network combo can result in confusing or broken experiences
731
+ * in the UI. The transactions are then updated using the BaseController update.
732
+ *
733
+ * @param transactions - The transactions to be applied to the state.
734
+ * @returns The trimmed list of transactions.
735
+ */
736
+ trimTransactionsForState(transactions) {
737
+ const nonceNetworkSet = new Set();
738
+ const txsToKeep = transactions.reverse().filter((tx) => {
739
+ const { chainId, networkID, status, transaction, time } = tx;
740
+ if (transaction) {
741
+ const key = `${transaction.nonce}-${chainId !== null && chainId !== void 0 ? chainId : networkID}-${new Date(time).toDateString()}`;
742
+ if (nonceNetworkSet.has(key)) {
743
+ return true;
744
+ }
745
+ else if (nonceNetworkSet.size < this.config.txHistoryLimit ||
746
+ !this.isFinalState(status)) {
747
+ nonceNetworkSet.add(key);
748
+ return true;
749
+ }
750
+ }
751
+ return false;
752
+ });
753
+ txsToKeep.reverse();
754
+ return txsToKeep;
755
+ }
756
+ /**
757
+ * Determines if the transaction is in a final state.
758
+ *
759
+ * @param status - The transaction status.
760
+ * @returns Whether the transaction is in a final state.
761
+ */
762
+ isFinalState(status) {
763
+ return (status === TransactionStatus.rejected ||
764
+ status === TransactionStatus.confirmed ||
765
+ status === TransactionStatus.failed ||
766
+ status === TransactionStatus.cancelled);
767
+ }
768
+ /**
769
+ * Method to verify the state of a transaction using the Blockchain as a source of truth.
770
+ *
771
+ * @param meta - The local transaction to verify on the blockchain.
772
+ * @returns A tuple containing the updated transaction, and whether or not an update was required.
773
+ */
774
+ blockchainTransactionStateReconciler(meta) {
775
+ return __awaiter(this, void 0, void 0, function* () {
776
+ const { status, transactionHash } = meta;
777
+ switch (status) {
778
+ case TransactionStatus.confirmed:
779
+ const txReceipt = yield (0, controller_utils_1.query)(this.ethQuery, 'getTransactionReceipt', [
780
+ transactionHash,
781
+ ]);
782
+ if (!txReceipt) {
783
+ return [meta, false];
784
+ }
785
+ meta.verifiedOnBlockchain = true;
786
+ meta.transaction.gasUsed = txReceipt.gasUsed;
787
+ // According to the Web3 docs:
788
+ // TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.
789
+ if (Number(txReceipt.status) === 0) {
790
+ const error = new Error('Transaction failed. The transaction was reversed');
791
+ this.failTransaction(meta, error);
792
+ return [meta, false];
793
+ }
794
+ return [meta, true];
795
+ case TransactionStatus.submitted:
796
+ const txObj = yield (0, controller_utils_1.query)(this.ethQuery, 'getTransactionByHash', [
797
+ transactionHash,
798
+ ]);
799
+ if (!txObj) {
800
+ const receiptShowsFailedStatus = yield this.checkTxReceiptStatusIsFailed(transactionHash);
801
+ // Case the txObj is evaluated as false, a second check will
802
+ // determine if the tx failed or it is pending or confirmed
803
+ if (receiptShowsFailedStatus) {
804
+ const error = new Error('Transaction failed. The transaction was dropped or replaced by a new one');
805
+ this.failTransaction(meta, error);
806
+ }
807
+ }
808
+ /* istanbul ignore next */
809
+ if (txObj === null || txObj === void 0 ? void 0 : txObj.blockNumber) {
810
+ meta.status = TransactionStatus.confirmed;
811
+ this.hub.emit(`${meta.id}:confirmed`, meta);
812
+ return [meta, true];
813
+ }
814
+ return [meta, false];
815
+ default:
816
+ return [meta, false];
817
+ }
818
+ });
819
+ }
820
+ /**
821
+ * Method to check if a tx has failed according to their receipt
822
+ * According to the Web3 docs:
823
+ * TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.
824
+ * The receipt is not available for pending transactions and returns null.
825
+ *
826
+ * @param txHash - The transaction hash.
827
+ * @returns Whether the transaction has failed.
828
+ */
829
+ checkTxReceiptStatusIsFailed(txHash) {
830
+ return __awaiter(this, void 0, void 0, function* () {
831
+ const txReceipt = yield (0, controller_utils_1.query)(this.ethQuery, 'getTransactionReceipt', [
832
+ txHash,
833
+ ]);
834
+ if (!txReceipt) {
835
+ // Transaction is pending
836
+ return false;
837
+ }
838
+ return Number(txReceipt.status) === 0;
839
+ });
840
+ }
841
+ /**
842
+ * Method to verify the state of transactions using Etherscan as a source of truth.
843
+ *
844
+ * @param remoteTxs - Transactions to reconcile that are from a remote source.
845
+ * @param localTxs - Transactions to reconcile that are local.
846
+ * @returns A tuple containing a boolean indicating whether or not an update was required, and the updated transaction.
847
+ */
848
+ etherscanTransactionStateReconciler(remoteTxs, localTxs) {
849
+ const updatedTxs = this.getUpdatedTransactions(remoteTxs, localTxs);
850
+ const newTxs = this.getNewTransactions(remoteTxs, localTxs);
851
+ const updatedLocalTxs = localTxs.map((tx) => {
852
+ const txIdx = updatedTxs.findIndex(({ transactionHash }) => transactionHash === tx.transactionHash);
853
+ return txIdx === -1 ? tx : updatedTxs[txIdx];
854
+ });
855
+ const updateRequired = newTxs.length > 0 || updatedLocalTxs.length > 0;
856
+ return [updateRequired, [...newTxs, ...updatedLocalTxs]];
857
+ }
858
+ /**
859
+ * Get all transactions that are in the remote transactions array
860
+ * but not in the local transactions array.
861
+ *
862
+ * @param remoteTxs - Array of transactions from remote source.
863
+ * @param localTxs - Array of transactions stored locally.
864
+ * @returns The new transactions.
865
+ */
866
+ getNewTransactions(remoteTxs, localTxs) {
867
+ return remoteTxs.filter((tx) => {
868
+ const alreadyInTransactions = localTxs.find(({ transactionHash }) => transactionHash === tx.transactionHash);
869
+ return !alreadyInTransactions;
870
+ });
871
+ }
872
+ /**
873
+ * Get all the transactions that are locally outdated with respect
874
+ * to a remote source (etherscan or blockchain). The returned array
875
+ * contains the transactions with the updated data.
876
+ *
877
+ * @param remoteTxs - Array of transactions from remote source.
878
+ * @param localTxs - Array of transactions stored locally.
879
+ * @returns The updated transactions.
880
+ */
881
+ getUpdatedTransactions(remoteTxs, localTxs) {
882
+ return remoteTxs.filter((remoteTx) => {
883
+ const isTxOutdated = localTxs.find((localTx) => {
884
+ return (remoteTx.transactionHash === localTx.transactionHash &&
885
+ this.isTransactionOutdated(remoteTx, localTx));
886
+ });
887
+ return isTxOutdated;
888
+ });
889
+ }
890
+ /**
891
+ * Verifies if a local transaction is outdated with respect to the remote transaction.
892
+ *
893
+ * @param remoteTx - The remote transaction from Etherscan.
894
+ * @param localTx - The local transaction.
895
+ * @returns Whether the transaction is outdated.
896
+ */
897
+ isTransactionOutdated(remoteTx, localTx) {
898
+ const statusOutdated = this.isStatusOutdated(remoteTx.transactionHash, localTx.transactionHash, remoteTx.status, localTx.status);
899
+ const gasDataOutdated = this.isGasDataOutdated(remoteTx.transaction.gasUsed, localTx.transaction.gasUsed);
900
+ return statusOutdated || gasDataOutdated;
901
+ }
902
+ /**
903
+ * Verifies if the status of a local transaction is outdated with respect to the remote transaction.
904
+ *
905
+ * @param remoteTxHash - Remote transaction hash.
906
+ * @param localTxHash - Local transaction hash.
907
+ * @param remoteTxStatus - Remote transaction status.
908
+ * @param localTxStatus - Local transaction status.
909
+ * @returns Whether the status is outdated.
910
+ */
911
+ isStatusOutdated(remoteTxHash, localTxHash, remoteTxStatus, localTxStatus) {
912
+ return remoteTxHash === localTxHash && remoteTxStatus !== localTxStatus;
913
+ }
914
+ /**
915
+ * Verifies if the gas data of a local transaction is outdated with respect to the remote transaction.
916
+ *
917
+ * @param remoteGasUsed - Remote gas used in the transaction.
918
+ * @param localGasUsed - Local gas used in the transaction.
919
+ * @returns Whether the gas data is outdated.
920
+ */
921
+ isGasDataOutdated(remoteGasUsed, localGasUsed) {
922
+ return remoteGasUsed !== localGasUsed;
923
+ }
924
+ }
925
+ exports.TransactionController = TransactionController;
926
+ exports.default = TransactionController;
927
+ //# sourceMappingURL=TransactionController.js.map