From e351a6bbe2173568ecf3c19ba6158491d47a820f Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 3 Aug 2023 13:42:44 +0200 Subject: [PATCH] fix: minimise public api of tx controller (#20361) --- app/scripts/controllers/transactions/index.js | 1150 ++++++++--------- .../controllers/transactions/index.test.js | 52 +- 2 files changed, 599 insertions(+), 603 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index c2eeaa0f6..bf6e7be45 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -237,93 +237,6 @@ export default class TransactionController extends EventEmitter { ///: END:ONLY_INCLUDE_IN } - /** - * Gets the current chainId in the network store as a number, returning 0 if - * the chainId parses to NaN. - * - * @returns {number} The numerical chainId. - */ - getChainId() { - const networkStatus = this.getNetworkStatus(); - const chainId = this._getCurrentChainId(); - const integerChainId = parseInt(chainId, 16); - if ( - networkStatus !== NetworkStatus.Available || - Number.isNaN(integerChainId) - ) { - return 0; - } - return integerChainId; - } - - async getEIP1559Compatibility(fromAddress) { - const currentNetworkIsCompatible = - await this._getCurrentNetworkEIP1559Compatibility(); - const fromAccountIsCompatible = - await this._getCurrentAccountEIP1559Compatibility(fromAddress); - return currentNetworkIsCompatible && fromAccountIsCompatible; - } - - /** - * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for - * specifying which chain, network, hardfork and EIPs to support for - * a transaction. By referencing this configuration, and analyzing the fields - * specified in txParams, `@ethereumjs/tx` is able to determine which EIP-2718 - * transaction type to use. - * - * @param fromAddress - * @returns {Common} common configuration object - */ - async getCommonConfiguration(fromAddress) { - const { type, nickname: name } = this.getProviderConfig(); - const supportsEIP1559 = await this.getEIP1559Compatibility(fromAddress); - - // This logic below will have to be updated each time a hardfork happens - // that carries with it a new Transaction type. It is inconsequential for - // hardforks that do not include new types. - const hardfork = supportsEIP1559 ? Hardfork.London : Hardfork.Berlin; - - // type will be one of our default network names or 'rpc'. the default - // network names are sufficient configuration, simply pass the name as the - // chain argument in the constructor. - if ( - type !== NETWORK_TYPES.RPC && - type !== NETWORK_TYPES.SEPOLIA && - type !== NETWORK_TYPES.LINEA_GOERLI && - type !== NETWORK_TYPES.LINEA_MAINNET - ) { - return new Common({ - chain: type, - hardfork, - }); - } - - // For 'rpc' we need to use the same basic configuration as mainnet, since - // we only support EVM compatible chains, and then override the - // name, chainId and networkId properties. This is done using the - // `forCustomChain` static method on the Common class. - const chainId = parseInt(this._getCurrentChainId(), 16); - const networkStatus = this.getNetworkStatus(); - const networkId = this.getNetworkId(); - - return Common.custom({ - name, - chainId, - // It is improbable for a transaction to be signed while the network - // is loading for two reasons. - // 1. Pending, unconfirmed transactions are wiped on network change - // 2. The UI is unusable (loading indicator) when network is loading. - // setting the networkId to 0 is for type safety and to explicity lead - // the transaction to failing if a user is able to get to this branch - // on a custom network that requires valid network id. I have not ran - // into this limitation on any network I have attempted, even when - // hardcoding networkId to 'loading'. - networkId: - networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0, - hardfork, - }); - } - /** * Wipes the transactions for a given account * @@ -404,48 +317,6 @@ export default class TransactionController extends EventEmitter { }); } - // ==================================================================================================================================================== - - /** - * @param {number} txId - * @returns {TransactionMeta} the txMeta who matches the given id if none found - * for the network returns undefined - */ - _getTransaction(txId) { - const { transactions } = this.store.getState(); - return transactions[txId]; - } - - /** - * @param {number} txId - * @returns {boolean} - */ - _isUnapprovedTransaction(txId) { - return ( - this.txStateManager.getTransaction(txId).status === - TransactionStatus.unapproved - ); - } - - /** - * @param {number} txId - * @param {string} fnName - */ - _throwErrorIfNotUnapprovedTx(txId, fnName) { - if (!this._isUnapprovedTransaction(txId)) { - throw new Error( - `TransactionsController: Can only call ${fnName} on an unapproved transaction. - Current tx status: ${this.txStateManager.getTransaction(txId).status}`, - ); - } - } - - _updateTransaction(txId, proposedUpdate, note) { - const txMeta = this.txStateManager.getTransaction(txId); - const updated = merge(txMeta, proposedUpdate); - this.txStateManager.updateTransaction(updated, note); - } - /** * updates the params that are editible in the send edit flow * @@ -611,7 +482,375 @@ export default class TransactionController extends EventEmitter { return this._getTransaction(txId); } - async addTransactionGasDefaults(txMeta) { + /** + * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The + * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to + * the sender's address, and has a higher gasPrice than that of the previous transaction. + * + * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel + * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas + * params instead of allowing this method to generate them + * @param options + * @param options.estimatedBaseFee + * @param options.actionId + * @returns {txMeta} + */ + async createCancelTransaction( + originalTxId, + customGasSettings, + { estimatedBaseFee, actionId } = {}, + ) { + // If transaction is found for same action id, do not create a new cancel transaction. + if (actionId) { + const existingTxMeta = + this.txStateManager.getTransactionWithActionId(actionId); + if (existingTxMeta) { + return existingTxMeta; + } + } + + const originalTxMeta = this.txStateManager.getTransaction(originalTxId); + const { txParams } = originalTxMeta; + const { from, nonce } = txParams; + + const { previousGasParams, newGasParams } = this._generateNewGasParams( + originalTxMeta, + { + ...customGasSettings, + // We want to override the previous transactions gasLimit because it + // will now be a simple send instead of whatever it was before such + // as a token transfer or contract call. + gasLimit: customGasSettings.gasLimit || GAS_LIMITS.SIMPLE, + }, + ); + + const newTxMeta = this.txStateManager.generateTxMeta({ + txParams: { + from, + to: from, + nonce, + value: '0x0', + ...newGasParams, + }, + previousGasParams, + loadingDefaults: false, + status: TransactionStatus.approved, + type: TransactionType.cancel, + actionId, + }); + + if (estimatedBaseFee) { + newTxMeta.estimatedBaseFee = estimatedBaseFee; + } + + this._addTransaction(newTxMeta); + await this._approveTransaction(newTxMeta.id, actionId, { + hasApprovalRequest: false, + }); + return newTxMeta; + } + + /** + * Creates a new approved transaction to attempt to speed up a previously submitted transaction. The + * new transaction contains the same nonce as the previous. By default, the new transaction will use + * the same gas limit and a 10% higher gas price, though it is possible to set a custom value for + * each instead. + * + * @param {number} originalTxId - the id of the txMeta that you want to speed up + * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas + * params instead of allowing this method to generate them + * @param options + * @param options.estimatedBaseFee + * @param options.actionId + * @returns {txMeta} + */ + async createSpeedUpTransaction( + originalTxId, + customGasSettings, + { estimatedBaseFee, actionId } = {}, + ) { + // If transaction is found for same action id, do not create a new speed-up transaction. + if (actionId) { + const existingTxMeta = + this.txStateManager.getTransactionWithActionId(actionId); + if (existingTxMeta) { + return existingTxMeta; + } + } + + const originalTxMeta = this.txStateManager.getTransaction(originalTxId); + const { txParams } = originalTxMeta; + + const { previousGasParams, newGasParams } = this._generateNewGasParams( + originalTxMeta, + customGasSettings, + ); + + const newTxMeta = this.txStateManager.generateTxMeta({ + txParams: { + ...txParams, + ...newGasParams, + }, + previousGasParams, + loadingDefaults: false, + status: TransactionStatus.approved, + type: TransactionType.retry, + originalType: originalTxMeta.type, + actionId, + }); + + if (estimatedBaseFee) { + newTxMeta.estimatedBaseFee = estimatedBaseFee; + } + + this._addTransaction(newTxMeta); + await this._approveTransaction(newTxMeta.id, actionId); + return newTxMeta; + } + + /** + * updates the txMeta in the txStateManager + * + * @param {object} txMeta - the updated txMeta + */ + async updateTransaction(txMeta) { + this.txStateManager.updateTransaction( + txMeta, + 'confTx: user updated transaction', + ); + } + + async approveTransactionsWithSameNonce(listOfTxParams = []) { + if (listOfTxParams.length === 0) { + return ''; + } + + const initialTx = listOfTxParams[0]; + const common = await this._getCommonConfiguration(initialTx.from); + const initialTxAsEthTx = TransactionFactory.fromTxData(initialTx, { + common, + }); + const initialTxAsSerializedHex = bufferToHex(initialTxAsEthTx.serialize()); + + if (this.inProcessOfSigning.has(initialTxAsSerializedHex)) { + return ''; + } + this.inProcessOfSigning.add(initialTxAsSerializedHex); + let rawTxes, nonceLock; + try { + // TODO: we should add a check to verify that all transactions have the same from address + const fromAddress = initialTx.from; + nonceLock = await this.nonceTracker.getNonceLock(fromAddress); + const nonce = nonceLock.nextNonce; + + rawTxes = await Promise.all( + listOfTxParams.map((txParams) => { + txParams.nonce = addHexPrefix(nonce.toString(16)); + return this._signExternalTransaction(txParams); + }), + ); + } catch (err) { + log.error(err); + // must set transaction to submitted/failed before releasing lock + // continue with error chain + throw err; + } finally { + if (nonceLock) { + nonceLock.releaseLock(); + } + this.inProcessOfSigning.delete(initialTxAsSerializedHex); + } + return rawTxes; + } + + async confirmExternalTransaction(txMeta, txReceipt, baseFeePerGas) { + // add external transaction + await this.txStateManager.addExternalTransaction(txMeta); + + if (!txMeta) { + return; + } + + const txId = txMeta.id; + + try { + const gasUsed = txUtils.normalizeTxReceiptGasUsed(txReceipt.gasUsed); + + txMeta.txReceipt = { + ...txReceipt, + gasUsed, + }; + + if (baseFeePerGas) { + txMeta.baseFeePerGas = baseFeePerGas; + } + + this.txStateManager.setTxStatusConfirmed(txId); + this._markNonceDuplicatesDropped(txId); + + const { submittedTime } = txMeta; + const metricsParams = { gas_used: gasUsed }; + + if (submittedTime) { + metricsParams.completion_time = + this._getTransactionCompletionTime(submittedTime); + } + + if (txReceipt.status === '0x0') { + metricsParams.status = METRICS_STATUS_FAILED; + // metricsParams.error = TODO: figure out a way to get the on-chain failure reason + } + + this._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.finalized, + undefined, + metricsParams, + ); + + this.txStateManager.updateTransaction( + txMeta, + 'transactions#confirmTransaction - add txReceipt', + ); + + if (txMeta.type === TransactionType.swap) { + await this._updatePostTxBalance({ + txMeta, + txId, + }); + } + } catch (err) { + log.error(err); + } + } + + /** + * Sets the txHas on the txMeta + * + * @param {number} txId - the tx's Id + * @param {string} txHash - the hash for the txMeta + */ + setTxHash(txId, txHash) { + // Add the tx hash to the persisted meta-tx object + const txMeta = this.txStateManager.getTransaction(txId); + txMeta.hash = txHash; + this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash'); + } + + /** + * Convenience method for the UI to easily create event fragments when the + * fragment does not exist in state. + * + * @param {number} transactionId - The transaction id to create the event + * fragment for + * @param {valueOf} event - event type to create + * @param {string} actionId - actionId passed from UI + */ + async createTransactionEventFragment(transactionId, event, actionId) { + const txMeta = this.txStateManager.getTransaction(transactionId); + const { properties, sensitiveProperties } = + await this._buildEventFragmentProperties(txMeta); + this._createTransactionEventFragment( + txMeta, + event, + properties, + sensitiveProperties, + actionId, + ); + } + + // + // PRIVATE METHODS + // + + /** + * Gets the current chainId in the network store as a number, returning 0 if + * the chainId parses to NaN. + * + * @returns {number} The numerical chainId. + */ + _getChainId() { + const networkStatus = this.getNetworkStatus(); + const chainId = this._getCurrentChainId(); + const integerChainId = parseInt(chainId, 16); + if ( + networkStatus !== NetworkStatus.Available || + Number.isNaN(integerChainId) + ) { + return 0; + } + return integerChainId; + } + + async _getEIP1559Compatibility(fromAddress) { + const currentNetworkIsCompatible = + await this._getCurrentNetworkEIP1559Compatibility(); + const fromAccountIsCompatible = + await this._getCurrentAccountEIP1559Compatibility(fromAddress); + return currentNetworkIsCompatible && fromAccountIsCompatible; + } + + /** + * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for + * specifying which chain, network, hardfork and EIPs to support for + * a transaction. By referencing this configuration, and analyzing the fields + * specified in txParams, `@ethereumjs/tx` is able to determine which EIP-2718 + * transaction type to use. + * + * @param fromAddress + * @returns {Common} common configuration object + */ + async _getCommonConfiguration(fromAddress) { + const { type, nickname: name } = this.getProviderConfig(); + const supportsEIP1559 = await this._getEIP1559Compatibility(fromAddress); + + // This logic below will have to be updated each time a hardfork happens + // that carries with it a new Transaction type. It is inconsequential for + // hardforks that do not include new types. + const hardfork = supportsEIP1559 ? Hardfork.London : Hardfork.Berlin; + + // type will be one of our default network names or 'rpc'. the default + // network names are sufficient configuration, simply pass the name as the + // chain argument in the constructor. + if ( + type !== NETWORK_TYPES.RPC && + type !== NETWORK_TYPES.SEPOLIA && + type !== NETWORK_TYPES.LINEA_GOERLI && + type !== NETWORK_TYPES.LINEA_MAINNET + ) { + return new Common({ + chain: type, + hardfork, + }); + } + + // For 'rpc' we need to use the same basic configuration as mainnet, since + // we only support EVM compatible chains, and then override the + // name, chainId and networkId properties. This is done using the + // `forCustomChain` static method on the Common class. + const chainId = parseInt(this._getCurrentChainId(), 16); + const networkStatus = this.getNetworkStatus(); + const networkId = this.getNetworkId(); + + return Common.custom({ + name, + chainId, + // It is improbable for a transaction to be signed while the network + // is loading for two reasons. + // 1. Pending, unconfirmed transactions are wiped on network change + // 2. The UI is unusable (loading indicator) when network is loading. + // setting the networkId to 0 is for type safety and to explicity lead + // the transaction to failing if a user is able to get to this branch + // on a custom network that requires valid network id. I have not ran + // into this limitation on any network I have attempted, even when + // hardcoding networkId to 'loading'. + networkId: + networkStatus === NetworkStatus.Available ? parseInt(networkId, 10) : 0, + hardfork, + }); + } + + async _addTransactionGasDefaults(txMeta) { const contractCode = await determineTransactionContractCode( txMeta.txParams, this.query, @@ -619,7 +858,7 @@ export default class TransactionController extends EventEmitter { let updateTxMeta = txMeta; try { - updateTxMeta = await this.addTxGasDefaults(txMeta, contractCode); + updateTxMeta = await this._addTxGasDefaults(txMeta, contractCode); } catch (error) { log.warn(error); updateTxMeta = this.txStateManager.getTransaction(txMeta.id); @@ -643,7 +882,78 @@ export default class TransactionController extends EventEmitter { return updateTxMeta; } - // ==================================================================================================================================================== + /** + * Sets the status of the transaction to confirmed and sets the status of nonce duplicates as + * dropped if the txParams have data it will fetch the txReceipt + * + * @param {number} txId - The tx's ID + * @param txReceipt + * @param baseFeePerGas + * @param blockTimestamp + * @returns {Promise} + */ + async _confirmTransaction(txId, txReceipt, baseFeePerGas, blockTimestamp) { + // get the txReceipt before marking the transaction confirmed + // to ensure the receipt is gotten before the ui revives the tx + const txMeta = this.txStateManager.getTransaction(txId); + + if (!txMeta) { + return; + } + + try { + const gasUsed = txUtils.normalizeTxReceiptGasUsed(txReceipt.gasUsed); + + txMeta.txReceipt = { + ...txReceipt, + gasUsed, + }; + + if (baseFeePerGas) { + txMeta.baseFeePerGas = baseFeePerGas; + } + if (blockTimestamp) { + txMeta.blockTimestamp = blockTimestamp; + } + + this.txStateManager.setTxStatusConfirmed(txId); + this._markNonceDuplicatesDropped(txId); + + const { submittedTime } = txMeta; + const metricsParams = { gas_used: gasUsed }; + + if (submittedTime) { + metricsParams.completion_time = + this._getTransactionCompletionTime(submittedTime); + } + + if (txReceipt.status === '0x0') { + metricsParams.status = METRICS_STATUS_FAILED; + // metricsParams.error = TODO: figure out a way to get the on-chain failure reason + } + + this._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.finalized, + undefined, + metricsParams, + ); + + this.txStateManager.updateTransaction( + txMeta, + 'transactions#confirmTransaction - add txReceipt', + ); + + if (txMeta.type === TransactionType.swap) { + await this._updatePostTxBalance({ + txMeta, + txId, + }); + } + } catch (err) { + log.error(err); + } + } /** * Adds the tx gas defaults: gas && gasPrice @@ -652,10 +962,10 @@ export default class TransactionController extends EventEmitter { * @param getCodeResponse * @returns {Promise} resolves with txMeta */ - async addTxGasDefaults(txMeta, getCodeResponse) { + async _addTxGasDefaults(txMeta, getCodeResponse) { const eip1559Compatibility = txMeta.txParams.type !== TransactionEnvelopeType.legacy && - (await this.getEIP1559Compatibility()); + (await this._getEIP1559Compatibility()); const { gasPrice: defaultGasPrice, maxFeePerGas: defaultMaxFeePerGas, @@ -876,6 +1186,120 @@ export default class TransactionController extends EventEmitter { return { gasLimit, simulationFails }; } + /** + * @param {number} txId + * @returns {TransactionMeta} the txMeta who matches the given id if none found + * for the network returns undefined + */ + _getTransaction(txId) { + const { transactions } = this.store.getState(); + return transactions[txId]; + } + + /** + * @param {number} txId + * @returns {boolean} + */ + _isUnapprovedTransaction(txId) { + return ( + this.txStateManager.getTransaction(txId).status === + TransactionStatus.unapproved + ); + } + + /** + * @param {number} txId + * @param {string} fnName + */ + _throwErrorIfNotUnapprovedTx(txId, fnName) { + if (!this._isUnapprovedTransaction(txId)) { + throw new Error( + `TransactionsController: Can only call ${fnName} on an unapproved transaction. + Current tx status: ${this.txStateManager.getTransaction(txId).status}`, + ); + } + } + + _updateTransaction(txId, proposedUpdate, note) { + const txMeta = this.txStateManager.getTransaction(txId); + const updated = merge(txMeta, proposedUpdate); + this.txStateManager.updateTransaction(updated, note); + } + + async _updatePostTxBalance({ txMeta, txId, numberOfAttempts = 6 }) { + const postTxBalance = await this.query.getBalance(txMeta.txParams.from); + const latestTxMeta = this.txStateManager.getTransaction(txId); + const approvalTxMeta = latestTxMeta.approvalTxId + ? this.txStateManager.getTransaction(latestTxMeta.approvalTxId) + : null; + latestTxMeta.postTxBalance = postTxBalance.toString(16); + const isDefaultTokenAddress = isSwapsDefaultTokenAddress( + txMeta.destinationTokenAddress, + txMeta.chainId, + ); + if ( + isDefaultTokenAddress && + txMeta.preTxBalance === latestTxMeta.postTxBalance && + numberOfAttempts > 0 + ) { + setTimeout(() => { + // If postTxBalance is the same as preTxBalance, try it again. + this._updatePostTxBalance({ + txMeta, + txId, + numberOfAttempts: numberOfAttempts - 1, + }); + }, UPDATE_POST_TX_BALANCE_TIMEOUT); + } else { + this.txStateManager.updateTransaction( + latestTxMeta, + 'transactions#confirmTransaction - add postTxBalance', + ); + this._trackSwapsMetrics(latestTxMeta, approvalTxMeta); + } + } + + /** + * publishes the raw tx and sets the txMeta to submitted + * + * @param {number} txId - the tx's Id + * @param {string} rawTx - the hex string of the serialized signed transaction + * @returns {Promise} + * @param {number} actionId - actionId passed from UI + */ + async _publishTransaction(txId, rawTx, actionId) { + const txMeta = this.txStateManager.getTransaction(txId); + txMeta.rawTx = rawTx; + if (txMeta.type === TransactionType.swap) { + const preTxBalance = await this.query.getBalance(txMeta.txParams.from); + txMeta.preTxBalance = preTxBalance.toString(16); + } + this.txStateManager.updateTransaction( + txMeta, + 'transactions#publishTransaction', + ); + let txHash; + try { + txHash = await this.query.sendRawTransaction(rawTx); + } catch (error) { + if (error.message.toLowerCase().includes('known transaction')) { + txHash = keccak(toBuffer(addHexPrefix(rawTx), 'hex')).toString('hex'); + txHash = addHexPrefix(txHash); + } else { + throw error; + } + } + this.setTxHash(txId, txHash); + + this.txStateManager.setTxStatusSubmitted(txId); + + this._trackTransactionMetricsEvent( + txMeta, + TransactionMetaMetricsEvent.submitted, + actionId, + ); + } + /** * Given a TransactionMeta object, generate new gas params such that if the * transaction was an EIP1559 transaction, it only has EIP1559 gas fields, @@ -892,7 +1316,7 @@ export default class TransactionController extends EventEmitter { * percentage bump of gas price. E.g 11 would be a 10% bump over base. * @returns {{ newGasParams: CustomGasSettings, previousGasParams: CustomGasSettings }} */ - generateNewGasParams( + _generateNewGasParams( originalTxMeta, customGasSettings = {}, incrementNumerator = 11, @@ -949,191 +1373,10 @@ export default class TransactionController extends EventEmitter { return { previousGasParams, newGasParams }; } - /** - * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The - * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to - * the sender's address, and has a higher gasPrice than that of the previous transaction. - * - * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel - * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas - * params instead of allowing this method to generate them - * @param options - * @param options.estimatedBaseFee - * @param options.actionId - * @returns {txMeta} - */ - async createCancelTransaction( - originalTxId, - customGasSettings, - { estimatedBaseFee, actionId } = {}, - ) { - // If transaction is found for same action id, do not create a new cancel transaction. - if (actionId) { - const existingTxMeta = - this.txStateManager.getTransactionWithActionId(actionId); - if (existingTxMeta) { - return existingTxMeta; - } - } - - const originalTxMeta = this.txStateManager.getTransaction(originalTxId); - const { txParams } = originalTxMeta; - const { from, nonce } = txParams; - - const { previousGasParams, newGasParams } = this.generateNewGasParams( - originalTxMeta, - { - ...customGasSettings, - // We want to override the previous transactions gasLimit because it - // will now be a simple send instead of whatever it was before such - // as a token transfer or contract call. - gasLimit: customGasSettings.gasLimit || GAS_LIMITS.SIMPLE, - }, - ); - - const newTxMeta = this.txStateManager.generateTxMeta({ - txParams: { - from, - to: from, - nonce, - value: '0x0', - ...newGasParams, - }, - previousGasParams, - loadingDefaults: false, - status: TransactionStatus.approved, - type: TransactionType.cancel, - actionId, - }); - - if (estimatedBaseFee) { - newTxMeta.estimatedBaseFee = estimatedBaseFee; - } - - this._addTransaction(newTxMeta); - await this._approveTransaction(newTxMeta.id, actionId, { - hasApprovalRequest: false, - }); - return newTxMeta; - } - - /** - * Creates a new approved transaction to attempt to speed up a previously submitted transaction. The - * new transaction contains the same nonce as the previous. By default, the new transaction will use - * the same gas limit and a 10% higher gas price, though it is possible to set a custom value for - * each instead. - * - * @param {number} originalTxId - the id of the txMeta that you want to speed up - * @param {CustomGasSettings} [customGasSettings] - overrides to use for gas - * params instead of allowing this method to generate them - * @param options - * @param options.estimatedBaseFee - * @param options.actionId - * @returns {txMeta} - */ - async createSpeedUpTransaction( - originalTxId, - customGasSettings, - { estimatedBaseFee, actionId } = {}, - ) { - // If transaction is found for same action id, do not create a new speed-up transaction. - if (actionId) { - const existingTxMeta = - this.txStateManager.getTransactionWithActionId(actionId); - if (existingTxMeta) { - return existingTxMeta; - } - } - - const originalTxMeta = this.txStateManager.getTransaction(originalTxId); - const { txParams } = originalTxMeta; - - const { previousGasParams, newGasParams } = this.generateNewGasParams( - originalTxMeta, - customGasSettings, - ); - - const newTxMeta = this.txStateManager.generateTxMeta({ - txParams: { - ...txParams, - ...newGasParams, - }, - previousGasParams, - loadingDefaults: false, - status: TransactionStatus.approved, - type: TransactionType.retry, - originalType: originalTxMeta.type, - actionId, - }); - - if (estimatedBaseFee) { - newTxMeta.estimatedBaseFee = estimatedBaseFee; - } - - this._addTransaction(newTxMeta); - await this._approveTransaction(newTxMeta.id, actionId); - return newTxMeta; - } - - /** - * updates the txMeta in the txStateManager - * - * @param {object} txMeta - the updated txMeta - */ - async updateTransaction(txMeta) { - this.txStateManager.updateTransaction( - txMeta, - 'confTx: user updated transaction', - ); - } - - async approveTransactionsWithSameNonce(listOfTxParams = []) { - if (listOfTxParams.length === 0) { - return ''; - } - - const initialTx = listOfTxParams[0]; - const common = await this.getCommonConfiguration(initialTx.from); - const initialTxAsEthTx = TransactionFactory.fromTxData(initialTx, { - common, - }); - const initialTxAsSerializedHex = bufferToHex(initialTxAsEthTx.serialize()); - - if (this.inProcessOfSigning.has(initialTxAsSerializedHex)) { - return ''; - } - this.inProcessOfSigning.add(initialTxAsSerializedHex); - let rawTxes, nonceLock; - try { - // TODO: we should add a check to verify that all transactions have the same from address - const fromAddress = initialTx.from; - nonceLock = await this.nonceTracker.getNonceLock(fromAddress); - const nonce = nonceLock.nextNonce; - - rawTxes = await Promise.all( - listOfTxParams.map((txParams) => { - txParams.nonce = addHexPrefix(nonce.toString(16)); - return this.signExternalTransaction(txParams); - }), - ); - } catch (err) { - log.error(err); - // must set transaction to submitted/failed before releasing lock - // continue with error chain - throw err; - } finally { - if (nonceLock) { - nonceLock.releaseLock(); - } - this.inProcessOfSigning.delete(initialTxAsSerializedHex); - } - return rawTxes; - } - - async signExternalTransaction(_txParams) { + async _signExternalTransaction(_txParams) { const normalizedTxParams = txUtils.normalizeTxParams(_txParams); // add network/chain id - const chainId = this.getChainId(); + const chainId = this._getChainId(); const type = isEIP1559Transaction({ txParams: normalizedTxParams }) ? TransactionEnvelopeType.feeMarket : TransactionEnvelopeType.legacy; @@ -1145,7 +1388,7 @@ export default class TransactionController extends EventEmitter { }; // sign tx const fromAddress = txParams.from; - const common = await this.getCommonConfiguration(fromAddress); + const common = await this._getCommonConfiguration(fromAddress); const unsignedEthTx = TransactionFactory.fromTxData(txParams, { common }); const signedEthTx = await this.signEthTx(unsignedEthTx, fromAddress); @@ -1159,10 +1402,10 @@ export default class TransactionController extends EventEmitter { * @param {number} txId - the tx's Id * @returns {string} rawTx */ - async signTransaction(txId) { + async _signTransaction(txId) { const txMeta = this.txStateManager.getTransaction(txId); // add network/chain id - const chainId = this.getChainId(); + const chainId = this._getChainId(); const type = isEIP1559Transaction(txMeta) ? TransactionEnvelopeType.feeMarket : TransactionEnvelopeType.legacy; @@ -1174,7 +1417,7 @@ export default class TransactionController extends EventEmitter { }; // sign tx const fromAddress = txParams.from; - const common = await this.getCommonConfiguration(txParams.from); + const common = await this._getCommonConfiguration(txParams.from); const unsignedEthTx = TransactionFactory.fromTxData(txParams, { common }); const signedEthTx = await this.signEthTx( unsignedEthTx, @@ -1215,253 +1458,6 @@ export default class TransactionController extends EventEmitter { return rawTx; } - /** - * publishes the raw tx and sets the txMeta to submitted - * - * @param {number} txId - the tx's Id - * @param {string} rawTx - the hex string of the serialized signed transaction - * @returns {Promise} - * @param {number} actionId - actionId passed from UI - */ - async publishTransaction(txId, rawTx, actionId) { - const txMeta = this.txStateManager.getTransaction(txId); - txMeta.rawTx = rawTx; - if (txMeta.type === TransactionType.swap) { - const preTxBalance = await this.query.getBalance(txMeta.txParams.from); - txMeta.preTxBalance = preTxBalance.toString(16); - } - this.txStateManager.updateTransaction( - txMeta, - 'transactions#publishTransaction', - ); - let txHash; - try { - txHash = await this.query.sendRawTransaction(rawTx); - } catch (error) { - if (error.message.toLowerCase().includes('known transaction')) { - txHash = keccak(toBuffer(addHexPrefix(rawTx), 'hex')).toString('hex'); - txHash = addHexPrefix(txHash); - } else { - throw error; - } - } - this.setTxHash(txId, txHash); - - this.txStateManager.setTxStatusSubmitted(txId); - - this._trackTransactionMetricsEvent( - txMeta, - TransactionMetaMetricsEvent.submitted, - actionId, - ); - } - - async updatePostTxBalance({ txMeta, txId, numberOfAttempts = 6 }) { - const postTxBalance = await this.query.getBalance(txMeta.txParams.from); - const latestTxMeta = this.txStateManager.getTransaction(txId); - const approvalTxMeta = latestTxMeta.approvalTxId - ? this.txStateManager.getTransaction(latestTxMeta.approvalTxId) - : null; - latestTxMeta.postTxBalance = postTxBalance.toString(16); - const isDefaultTokenAddress = isSwapsDefaultTokenAddress( - txMeta.destinationTokenAddress, - txMeta.chainId, - ); - if ( - isDefaultTokenAddress && - txMeta.preTxBalance === latestTxMeta.postTxBalance && - numberOfAttempts > 0 - ) { - setTimeout(() => { - // If postTxBalance is the same as preTxBalance, try it again. - this.updatePostTxBalance({ - txMeta, - txId, - numberOfAttempts: numberOfAttempts - 1, - }); - }, UPDATE_POST_TX_BALANCE_TIMEOUT); - } else { - this.txStateManager.updateTransaction( - latestTxMeta, - 'transactions#confirmTransaction - add postTxBalance', - ); - this._trackSwapsMetrics(latestTxMeta, approvalTxMeta); - } - } - - /** - * Sets the status of the transaction to confirmed and sets the status of nonce duplicates as - * dropped if the txParams have data it will fetch the txReceipt - * - * @param {number} txId - The tx's ID - * @param txReceipt - * @param baseFeePerGas - * @param blockTimestamp - * @returns {Promise} - */ - async confirmTransaction(txId, txReceipt, baseFeePerGas, blockTimestamp) { - // get the txReceipt before marking the transaction confirmed - // to ensure the receipt is gotten before the ui revives the tx - const txMeta = this.txStateManager.getTransaction(txId); - - if (!txMeta) { - return; - } - - try { - const gasUsed = txUtils.normalizeTxReceiptGasUsed(txReceipt.gasUsed); - - txMeta.txReceipt = { - ...txReceipt, - gasUsed, - }; - - if (baseFeePerGas) { - txMeta.baseFeePerGas = baseFeePerGas; - } - if (blockTimestamp) { - txMeta.blockTimestamp = blockTimestamp; - } - - this.txStateManager.setTxStatusConfirmed(txId); - this._markNonceDuplicatesDropped(txId); - - const { submittedTime } = txMeta; - const metricsParams = { gas_used: gasUsed }; - - if (submittedTime) { - metricsParams.completion_time = - this._getTransactionCompletionTime(submittedTime); - } - - if (txReceipt.status === '0x0') { - metricsParams.status = METRICS_STATUS_FAILED; - // metricsParams.error = TODO: figure out a way to get the on-chain failure reason - } - - this._trackTransactionMetricsEvent( - txMeta, - TransactionMetaMetricsEvent.finalized, - undefined, - metricsParams, - ); - - this.txStateManager.updateTransaction( - txMeta, - 'transactions#confirmTransaction - add txReceipt', - ); - - if (txMeta.type === TransactionType.swap) { - await this.updatePostTxBalance({ - txMeta, - txId, - }); - } - } catch (err) { - log.error(err); - } - } - - async confirmExternalTransaction(txMeta, txReceipt, baseFeePerGas) { - // add external transaction - await this.txStateManager.addExternalTransaction(txMeta); - - if (!txMeta) { - return; - } - - const txId = txMeta.id; - - try { - const gasUsed = txUtils.normalizeTxReceiptGasUsed(txReceipt.gasUsed); - - txMeta.txReceipt = { - ...txReceipt, - gasUsed, - }; - - if (baseFeePerGas) { - txMeta.baseFeePerGas = baseFeePerGas; - } - - this.txStateManager.setTxStatusConfirmed(txId); - this._markNonceDuplicatesDropped(txId); - - const { submittedTime } = txMeta; - const metricsParams = { gas_used: gasUsed }; - - if (submittedTime) { - metricsParams.completion_time = - this._getTransactionCompletionTime(submittedTime); - } - - if (txReceipt.status === '0x0') { - metricsParams.status = METRICS_STATUS_FAILED; - // metricsParams.error = TODO: figure out a way to get the on-chain failure reason - } - - this._trackTransactionMetricsEvent( - txMeta, - TransactionMetaMetricsEvent.finalized, - undefined, - metricsParams, - ); - - this.txStateManager.updateTransaction( - txMeta, - 'transactions#confirmTransaction - add txReceipt', - ); - - if (txMeta.type === TransactionType.swap) { - await this.updatePostTxBalance({ - txMeta, - txId, - }); - } - } catch (err) { - log.error(err); - } - } - - /** - * Sets the txHas on the txMeta - * - * @param {number} txId - the tx's Id - * @param {string} txHash - the hash for the txMeta - */ - setTxHash(txId, txHash) { - // Add the tx hash to the persisted meta-tx object - const txMeta = this.txStateManager.getTransaction(txId); - txMeta.hash = txHash; - this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash'); - } - - /** - * Convenience method for the UI to easily create event fragments when the - * fragment does not exist in state. - * - * @param {number} transactionId - The transaction id to create the event - * fragment for - * @param {valueOf} event - event type to create - * @param {string} actionId - actionId passed from UI - */ - async createTransactionEventFragment(transactionId, event, actionId) { - const txMeta = this.txStateManager.getTransaction(transactionId); - const { properties, sensitiveProperties } = - await this._buildEventFragmentProperties(txMeta); - this._createTransactionEventFragment( - txMeta, - event, - properties, - sensitiveProperties, - actionId, - ); - } - - // - // PRIVATE METHODS - // - _isTransactionCompleted(txMeta) { return [ TransactionStatus.submitted, @@ -1506,14 +1502,14 @@ export default class TransactionController extends EventEmitter { let existingTxMeta = this.txStateManager.getTransactionWithActionId(actionId); if (existingTxMeta) { - existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta); + existingTxMeta = await this._addTransactionGasDefaults(existingTxMeta); return { txMeta: existingTxMeta, isExisting: true }; } } // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams); - const eip1559Compatibility = await this.getEIP1559Compatibility(); + const eip1559Compatibility = await this._getEIP1559Compatibility(); txUtils.validateTxParams(normalizedTxParams, eip1559Compatibility); @@ -1581,7 +1577,7 @@ export default class TransactionController extends EventEmitter { this._addTransaction(txMeta); - txMeta = await this.addTransactionGasDefaults(txMeta); + txMeta = await this._addTransactionGasDefaults(txMeta); if ([TransactionType.swap, TransactionType.swapApproval].includes(type)) { txMeta = await this._createSwapsTransaction(swaps, type, txMeta); @@ -1783,7 +1779,7 @@ export default class TransactionController extends EventEmitter { // MMI does not broadcast transactions, as that is the responsibility of the custodian if (txMeta.custodyStatus) { this.inProcessOfSigning.delete(txId); - await this.signTransaction(txId); + await this._signTransaction(txId); return; } ///: END:ONLY_INCLUDE_IN @@ -1822,8 +1818,8 @@ export default class TransactionController extends EventEmitter { 'transactions#approveTransaction', ); // sign transaction - const rawTx = await this.signTransaction(txId); - await this.publishTransaction(txId, rawTx, actionId); + const rawTx = await this._signTransaction(txId); + await this._publishTransaction(txId, rawTx, actionId); this._trackTransactionMetricsEvent( txMeta, TransactionMetaMetricsEvent.approved, @@ -1921,7 +1917,7 @@ export default class TransactionController extends EventEmitter { }, }) .forEach((tx) => { - this.addTxGasDefaults(tx) + this._addTxGasDefaults(tx) .then((txMeta) => { txMeta.loadingDefaults = false; this.txStateManager.updateTransaction( @@ -1984,7 +1980,7 @@ export default class TransactionController extends EventEmitter { this.pendingTxTracker.on( 'tx:confirmed', (txId, transactionReceipt, baseFeePerGas, blockTimestamp) => - this.confirmTransaction( + this._confirmTransaction( txId, transactionReceipt, baseFeePerGas, diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index ee1af91f4..a94e331b1 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -750,11 +750,11 @@ describe('Transaction Controller', function () { providerResultStub.eth_estimateGas = '0x5209'; signStub = sinon - .stub(txController, 'signTransaction') + .stub(txController, '_signTransaction') .callsFake(() => Promise.resolve()); const pubStub = sinon - .stub(txController, 'publishTransaction') + .stub(txController, '_publishTransaction') .callsFake(() => { const txId = getLastTxMeta().id; txController.setTxHash(txId, originalValue); @@ -1212,7 +1212,7 @@ describe('Transaction Controller', function () { }); }); - describe('#addTxGasDefaults', function () { + describe('_addTxGasDefaults', function () { it('should add the tx defaults if their are none', async function () { txController.txStateManager._addTransactionsToState([ { @@ -1238,7 +1238,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }; providerResultStub.eth_estimateGas = '5209'; - const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta); + const txMetaWithDefaults = await txController._addTxGasDefaults(txMeta); assert.ok( txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price', @@ -1254,7 +1254,7 @@ describe('Transaction Controller', function () { const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400'; const stub1 = sinon - .stub(txController, 'getEIP1559Compatibility') + .stub(txController, '_getEIP1559Compatibility') .returns(true); const stub2 = sinon @@ -1287,7 +1287,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }; providerResultStub.eth_estimateGas = '5209'; - const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta); + const txMetaWithDefaults = await txController._addTxGasDefaults(txMeta); assert.equal( txMetaWithDefaults.txParams.maxFeePerGas, @@ -1307,7 +1307,7 @@ describe('Transaction Controller', function () { const TEST_GASPRICE = '0x12a05f200'; const stub1 = sinon - .stub(txController, 'getEIP1559Compatibility') + .stub(txController, '_getEIP1559Compatibility') .returns(true); const stub2 = sinon @@ -1337,7 +1337,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }; providerResultStub.eth_estimateGas = '5209'; - const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta); + const txMetaWithDefaults = await txController._addTxGasDefaults(txMeta); assert.equal( txMetaWithDefaults.txParams.maxFeePerGas, @@ -1357,7 +1357,7 @@ describe('Transaction Controller', function () { const TEST_GASPRICE = '0x12a05f200'; const stub1 = sinon - .stub(txController, 'getEIP1559Compatibility') + .stub(txController, '_getEIP1559Compatibility') .returns(true); const stub2 = sinon @@ -1389,7 +1389,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }; providerResultStub.eth_estimateGas = '5209'; - const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta); + const txMetaWithDefaults = await txController._addTxGasDefaults(txMeta); assert.equal( txMetaWithDefaults.txParams.maxFeePerGas, @@ -1411,7 +1411,7 @@ describe('Transaction Controller', function () { const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400'; const stub1 = sinon - .stub(txController, 'getEIP1559Compatibility') + .stub(txController, '_getEIP1559Compatibility') .returns(true); const stub2 = sinon @@ -1443,7 +1443,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }; providerResultStub.eth_estimateGas = '5209'; - const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta); + const txMetaWithDefaults = await txController._addTxGasDefaults(txMeta); assert.equal( txMetaWithDefaults.txParams.maxFeePerGas, @@ -1547,27 +1547,27 @@ describe('Transaction Controller', function () { }, noop, ); - const rawTx = await txController.signTransaction('1'); + const rawTx = await txController._signTransaction('1'); const ethTx = TransactionFactory.fromSerializedData(toBuffer(rawTx)); assert.equal(Number(ethTx.common.chainId()), 5); }); }); - describe('#getChainId', function () { + describe('_getChainId', function () { it('returns the chain ID of the network when it is available', function () { networkStatusStore.putState(NetworkStatus.Available); - assert.equal(txController.getChainId(), 5); + assert.equal(txController._getChainId(), 5); }); it('returns 0 when the network is not available', function () { - networkStatusStore.putState('asdflsfadf'); - assert.equal(txController.getChainId(), 0); + networkStatusStore.putState('NOT_INTEGER'); + assert.equal(txController._getChainId(), 0); }); it('returns 0 when the chain ID cannot be parsed as a hex string', function () { networkStatusStore.putState(NetworkStatus.Available); - getCurrentChainId.returns('$fdsjfldf'); - assert.equal(txController.getChainId(), 0); + getCurrentChainId.returns('NOT_INTEGER'); + assert.equal(txController._getChainId(), 0); }); }); @@ -1795,13 +1795,13 @@ describe('Transaction Controller', function () { }, }, ]); - await txController.signTransaction('1'); + await txController._signTransaction('1'); assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x0'); }); it('sets txParams.type to 0x2 (EIP-1559)', async function () { const eip1559CompatibilityStub = sinon - .stub(txController, 'getEIP1559Compatibility') + .stub(txController, '_getEIP1559Compatibility') .returns(true); txController.txStateManager._addTransactionsToState([ { @@ -1819,13 +1819,13 @@ describe('Transaction Controller', function () { }, }, ]); - await txController.signTransaction('2'); + await txController._signTransaction('2'); assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x2'); eip1559CompatibilityStub.restore(); }); }); - describe('#publishTransaction', function () { + describe('_publishTransaction', function () { let hash, txMeta, trackTransactionMetricsEventSpy; beforeEach(function () { @@ -1856,7 +1856,7 @@ describe('Transaction Controller', function () { const rawTx = '0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c'; txController.txStateManager.addTransaction(txMeta); - await txController.publishTransaction(txMeta.id, rawTx); + await txController._publishTransaction(txMeta.id, rawTx); const publishedTx = txController.txStateManager.getTransaction(1); assert.equal(publishedTx.hash, hash); assert.equal(publishedTx.status, TransactionStatus.submitted); @@ -1869,7 +1869,7 @@ describe('Transaction Controller', function () { const rawTx = '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a'; txController.txStateManager.addTransaction(txMeta); - await txController.publishTransaction(txMeta.id, rawTx); + await txController._publishTransaction(txMeta.id, rawTx); const publishedTx = txController.txStateManager.getTransaction(1); assert.equal( publishedTx.hash, @@ -1882,7 +1882,7 @@ describe('Transaction Controller', function () { const rawTx = '0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c'; txController.txStateManager.addTransaction(txMeta); - await txController.publishTransaction(txMeta.id, rawTx); + await txController._publishTransaction(txMeta.id, rawTx); assert.equal(trackTransactionMetricsEventSpy.callCount, 1); assert.deepEqual( trackTransactionMetricsEventSpy.getCall(0).args[0],