1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Fix gas api overcalling (#12069)

* Move gasEstimation calls onto edit-gas-popover instead of transaction list item

* delete useCancelTransaction and useRetryTransaction hooks
consolidate gasFee calls into cancel button component.

* add tests

* update component name

* addressing feedback

* fix failing e2e test

* followup fix e2e tests

* change useIncrementedGasFees to accept single transaction rather than transactionGroup as argument

* remove unnecessary change to fixture

* only ever pass primary transaction to useIncrementedGasFees

* remove unnecessary optional chaining
This commit is contained in:
Alex Donesky 2021-09-15 10:59:51 -05:00 committed by GitHub
parent 28fc2d471f
commit 74fa6fa187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 823 additions and 417 deletions

View File

@ -0,0 +1,234 @@
{
"hasCancelled": false,
"hasRetried": false,
"initialTransaction": {
"id": 6854191329910881,
"time": 1631558469046,
"status": "approved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": false,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther",
"history": [
{
"id": 6854191329910881,
"time": 1631558469046,
"status": "unapproved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": true,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1631558469059
},
{
"op": "add",
"path": "/userFeeLevel",
"value": "medium"
}
],
[
{
"op": "add",
"path": "/estimatedBaseFee",
"value": "0",
"note": "confTx: user approved transaction",
"timestamp": 1631558472917
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved",
"timestamp": 1631558472925
}
]
],
"userFeeLevel": "medium",
"estimatedBaseFee": "0"
},
"primaryTransaction": {
"id": 6854191329910881,
"time": 1631558469046,
"status": "approved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": false,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther",
"history": [
{
"id": 6854191329910881,
"time": 1631558469046,
"status": "unapproved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": true,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1631558469059
},
{
"op": "add",
"path": "/userFeeLevel",
"value": "medium"
}
],
[
{
"op": "add",
"path": "/estimatedBaseFee",
"value": "0",
"note": "confTx: user approved transaction",
"timestamp": 1631558472917
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved",
"timestamp": 1631558472925
}
]
],
"userFeeLevel": "medium",
"estimatedBaseFee": "0"
},
"transactions": [
{
"id": 6854191329910881,
"time": 1631558469046,
"status": "approved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": false,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther",
"history": [
{
"id": 6854191329910881,
"time": 1631558469046,
"status": "unapproved",
"metamaskNetworkId": "42",
"chainId": "0x2a",
"loadingDefaults": true,
"dappSuggestedGasFees": null,
"txParams": {
"from": "0x0853dccd30e0582df80b16ec014092160b48e797",
"to": "0x8d09d17af2e20f51a9b598cb9edd01489a26ae27",
"value": "0x38d7ea4c68000",
"gas": "0x5208",
"maxFeePerGas": "0x47868c00",
"maxPriorityFeePerGas": "0x47868c00",
"type": "0x2"
},
"origin": "metamask",
"type": "sentEther"
},
[
{
"op": "replace",
"path": "/loadingDefaults",
"value": false,
"note": "Added new unapproved transaction.",
"timestamp": 1631558469059
},
{
"op": "add",
"path": "/userFeeLevel",
"value": "medium"
}
],
[
{
"op": "add",
"path": "/estimatedBaseFee",
"value": "0",
"note": "confTx: user approved transaction",
"timestamp": 1631558472917
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved",
"timestamp": 1631558472925
}
]
],
"userFeeLevel": "medium",
"estimatedBaseFee": "0"
}
]
}

View File

@ -61,6 +61,68 @@
"transactionIndex": "0"
}
},
"transactions": [
{
"id": 4243712234858512,
"time": 1589314601567,
"status": "confirmed",
"metamaskNetworkId": "4",
"loadingDefaults": false,
"txParams": {
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97",
"nonce": "0xc",
"value": "0xde0b6b3a7640000",
"gas": "0x5208",
"gasPrice": "0x2540be400"
},
"origin": "metamask",
"type": "sentEther",
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 12,
"highestSuggested": 12,
"nextNetworkNonce": 12
},
"local": {
"name": "local",
"nonce": 12,
"details": {
"startPoint": 12,
"highest": 12
}
},
"network": {
"name": "network",
"nonce": 12,
"details": {
"blockNumber": "0x62d5dc",
"baseCount": 12
}
}
},
"r": "0xe0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595",
"s": "0x1c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58",
"v": "0x2c",
"rawTx": "0xf86c0c8502540be40082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97880de0b6b3a7640000802ca0e0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595a01c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58",
"hash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2",
"submittedTime": 1589314602908,
"txReceipt": {
"blockHash": "0xb9d2d71153b66146fde74b14b1c1ffc0588eb4a02ff464e32a4db9ae4bbfad8a",
"blockNumber": "62d5de",
"contractAddress": null,
"cumulativeGasUsed": "5208",
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"gasUsed": "5208",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97",
"transactionHash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2",
"transactionIndex": "0"
}
}
],
"primaryTransaction": {
"id": 4243712234858512,
"time": 1589314601567,
@ -186,6 +248,68 @@
"transactionIndex": "0"
}
},
"transactions": [
{
"id": 4243712234858507,
"time": 1589314355872,
"status": "confirmed",
"metamaskNetworkId": "4",
"loadingDefaults": false,
"txParams": {
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848",
"nonce": "0xb",
"value": "0x1bc16d674ec80000",
"gas": "0x5208",
"gasPrice": "0x2540be400"
},
"origin": "metamask",
"type": "sentEther",
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 0,
"highestSuggested": 10,
"nextNetworkNonce": 10
},
"local": {
"name": "local",
"nonce": 11,
"details": {
"startPoint": 10,
"highest": 11
}
},
"network": {
"name": "network",
"nonce": 10,
"details": {
"blockNumber": "0x62d5cc",
"baseCount": 10
}
}
},
"r": "0xe6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055",
"s": "0x10613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458",
"v": "0x2b",
"rawTx": "0xf86c0b8502540be400825208940ccc8aeeaf5ce790f3b448325981a143fdef8848881bc16d674ec80000802ba0e6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055a010613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458",
"hash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd",
"submittedTime": 1589314356907,
"txReceipt": {
"blockHash": "0xfa3c8b63aaba2ef64ab328af72811dd5110a7641bd435cc6fbdfd9ea0d334542",
"blockNumber": "62d5ce",
"contractAddress": null,
"cumulativeGasUsed": "5208",
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"gasUsed": "5208",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848",
"transactionHash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd",
"transactionIndex": "0"
}
}
],
"primaryTransaction": {
"id": 4243712234858507,
"time": 1589314355872,
@ -312,6 +436,69 @@
"transactionIndex": "1"
}
},
"transactions": [
{
"id": 4243712234858506,
"time": 1589314345433,
"status": "confirmed",
"metamaskNetworkId": "4",
"loadingDefaults": false,
"txParams": {
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97",
"nonce": "0xa",
"value": "0x1bc16d674ec80000",
"gas": "0x5208",
"gasPrice": "0x306dc4200"
},
"origin": "metamask",
"type": "sentEther",
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 0,
"highestSuggested": 10,
"nextNetworkNonce": 10
},
"local": {
"name": "local",
"nonce": 10,
"details": {
"startPoint": 10,
"highest": 10
}
},
"network": {
"name": "network",
"nonce": 10,
"details": {
"blockNumber": "0x62d5cb",
"baseCount": 10
}
}
},
"r": "0x94b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3b",
"s": "0x1778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f",
"v": "0x2c",
"rawTx": "0xf86c0a850306dc420082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97881bc16d674ec80000802ca094b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3ba01778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f",
"hash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536",
"submittedTime": 1589314348235,
"firstRetryBlockNumber": "0x62d5cc",
"txReceipt": {
"blockHash": "0x3d61a8d8a0e79e0e7a3a9206bf62f9a8e47791c527cd85cb4fcf800609234115",
"blockNumber": "62d5cd",
"contractAddress": null,
"cumulativeGasUsed": "a810",
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"gasUsed": "5208",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": "0x1",
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97",
"transactionHash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536",
"transactionIndex": "1"
}
}
],
"primaryTransaction": {
"id": 4243712234858506,
"time": 1589314345433,
@ -394,6 +581,25 @@
"hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116",
"type": "incoming"
},
"transactions": [
{
"blockNumber": "6477257",
"id": 4243712234858505,
"metamaskNetworkId": "4",
"status": "confirmed",
"time": 1589314295000,
"txParams": {
"from": "0x31b98d14007bdee637298086988a0bbd31184523",
"gas": "0x5208",
"gasPrice": "0x3b9aca00",
"nonce": "0x56540",
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"value": "0x1043561a882930000"
},
"hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116",
"type": "incoming"
}
],
"primaryTransaction": {
"blockNumber": "6477257",
"id": 4243712234858505,
@ -432,6 +638,25 @@
"hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889",
"type": "incoming"
},
"transactions": [
{
"blockNumber": "6454493",
"id": 4243712234858475,
"metamaskNetworkId": "4",
"status": "confirmed",
"time": 1588972833000,
"txParams": {
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"gas": "0x5208",
"gasPrice": "0x24e160300",
"nonce": "0x8",
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"value": "0x0"
},
"hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889",
"type": "incoming"
}
],
"primaryTransaction": {
"blockNumber": "6454493",
"id": 4243712234858475,
@ -470,6 +695,25 @@
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a",
"type": "incoming"
},
"transactions": [
{
"blockNumber": "6195526",
"id": 4243712234858466,
"metamaskNetworkId": "4",
"status": "confirmed",
"time": 1585087013000,
"txParams": {
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb",
"gas": "0x5208",
"gasPrice": "0x77359400",
"nonce": "0x3",
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149",
"value": "0xde0b6b3a7640000"
},
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a",
"type": "incoming"
}
],
"primaryTransaction": {
"blockNumber": "6195526",
"id": 4243712234858466,
@ -510,6 +754,28 @@
"sourceTokenSymbol": "ETH",
"type": "swap"
},
"transactions": [
{
"blockNumber": "6195527",
"id": 4243712234858467,
"metamaskNetworkId": "4",
"status": "confirmed",
"time": 1585088013000,
"txParams": {
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb",
"gas": "0x5208",
"gasPrice": "0x77359400",
"nonce": "0x3",
"to": "0xabca64466f257793eaa52fcfff5066894b76a149",
"value": "0xde0b6b3a7640000"
},
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a",
"type": "swap",
"destinationTokenSymbol": "ABC",
"destinationTokenAddress": "0xabca64466f257793eaa52fcfff5066894b76a149",
"sourceTokenSymbol": "ETH"
}
],
"primaryTransaction": {
"blockNumber": "6195527",
"id": 4243712234858467,

View File

@ -130,6 +130,23 @@
"transactions": {
"4046084157914634": {
"chainId": "0x539",
"primaryTransaction": {
"chainId": "0x539",
"id": 4046084157914634,
"loadingDefaults": true,
"metamaskNetworkId": "1337",
"origin": "metamask",
"status": "unapproved",
"time": 1617228030067,
"txParams": {
"from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1",
"gas": "0x61a8",
"gasPrice": "0x2540be400",
"to": "0x2f318C334780961FB129D2a6c30D0763d9a5C970",
"value": "0xde0b6b3a7640000"
},
"type": "sentEther"
},
"history": [
{
"chainId": "0x539",

View File

@ -0,0 +1,64 @@
import { Tooltip } from '@material-ui/core';
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import classnames from 'classnames';
import Button from '../../ui/button';
import { getMaximumGasTotalInHexWei } from '../../../../shared/modules/gas.utils';
import { getConversionRate } from '../../../ducks/metamask/metamask';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useIncrementedGasFees } from '../../../hooks/useIncrementedGasFees';
import { isBalanceSufficient } from '../../../pages/send/send.utils';
import { getSelectedAccount } from '../../../selectors';
export default function CancelButton({
cancelTransaction,
transaction,
detailsModal,
}) {
const t = useI18nContext();
const customCancelGasSettings = useIncrementedGasFees(transaction);
const selectedAccount = useSelector(getSelectedAccount);
const conversionRate = useSelector(getConversionRate);
const hasEnoughCancelGas = isBalanceSufficient({
amount: '0x0',
gasTotal: getMaximumGasTotalInHexWei(customCancelGasSettings),
balance: selectedAccount.balance,
conversionRate,
});
const btn = (
<Button
onClick={cancelTransaction}
rounded={!detailsModal}
type={detailsModal ? 'raise' : null}
className={classnames({
'transaction-list-item__header-button': !detailsModal,
'transaction-list-item-details__header-button': detailsModal,
})}
disabled={!hasEnoughCancelGas}
>
{t('cancel')}
</Button>
);
return hasEnoughCancelGas ? (
btn
) : (
<Tooltip
title={t('notEnoughGas')}
data-testid="not-enough-gas__tooltip"
position="bottom"
>
<div>{btn}</div>
</Tooltip>
);
}
CancelButton.propTypes = {
transaction: PropTypes.object,
cancelTransaction: PropTypes.func,
detailsModal: PropTypes.bool,
};

View File

@ -0,0 +1 @@
export { default } from './cancel-button';

View File

@ -28,6 +28,7 @@ import {
} from '../../../store/actions';
import LoadingHeartBeat from '../../ui/loading-heartbeat';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import { useIncrementedGasFees } from '../../../hooks/useIncrementedGasFees';
export default function EditGasPopover({
popoverTitle = '',
@ -62,6 +63,19 @@ export default function EditGasPopover({
] = useState(false);
const minimumGasLimitDec = hexToDecimal(minimumGasLimit);
const updatedCustomGasSettings = useIncrementedGasFees(transaction);
let updatedTransaction = transaction;
if (mode === EDIT_GAS_MODES.SPEED_UP || mode === EDIT_GAS_MODES.CANCEL) {
updatedTransaction = {
...transaction,
userFeeLevel: 'custom',
txParams: {
...transaction.txParams,
...updatedCustomGasSettings,
},
};
}
const {
maxPriorityFeePerGas,
@ -89,10 +103,15 @@ export default function EditGasPopover({
balanceError,
estimatesUnavailableWarning,
estimatedBaseFee,
} = useGasFeeInputs(defaultEstimateToUse, transaction, minimumGasLimit, mode);
} = useGasFeeInputs(
defaultEstimateToUse,
updatedTransaction,
minimumGasLimit,
mode,
);
const txParamsHaveBeenCustomized =
estimateToUse === 'custom' || txParamsAreDappSuggested(transaction);
estimateToUse === 'custom' || txParamsAreDappSuggested(updatedTransaction);
/**
* Temporary placeholder, this should be managed by the parent component but
@ -109,7 +128,7 @@ export default function EditGasPopover({
}, [onClose, dispatch]);
const onSubmit = useCallback(() => {
if (!transaction || !mode) {
if (!updatedTransaction || !mode) {
closePopover();
}
@ -128,14 +147,14 @@ export default function EditGasPopover({
gasPrice: decGWEIToHexWEI(gasPrice),
};
const cleanTransactionParams = { ...transaction.txParams };
const cleanTransactionParams = { ...updatedTransaction.txParams };
if (networkAndAccountSupport1559) {
delete cleanTransactionParams.gasPrice;
}
const updatedTxMeta = {
...transaction,
...updatedTransaction,
userFeeLevel: estimateToUse || 'custom',
txParams: {
...cleanTransactionParams,
@ -146,14 +165,14 @@ export default function EditGasPopover({
switch (mode) {
case EDIT_GAS_MODES.CANCEL:
dispatch(
createCancelTransaction(transaction.id, newGasSettings, {
createCancelTransaction(updatedTransaction.id, newGasSettings, {
estimatedBaseFee,
}),
);
break;
case EDIT_GAS_MODES.SPEED_UP:
dispatch(
createSpeedUpTransaction(transaction.id, newGasSettings, {
createSpeedUpTransaction(updatedTransaction.id, newGasSettings, {
estimatedBaseFee,
}),
);
@ -174,7 +193,7 @@ export default function EditGasPopover({
closePopover();
}, [
transaction,
updatedTransaction,
mode,
dispatch,
closePopover,
@ -259,7 +278,7 @@ export default function EditGasPopover({
estimatedMaximumFiat={estimatedMaximumFiat}
onEducationClick={() => setShowEducationContent(true)}
mode={mode}
transaction={transaction}
transaction={updatedTransaction}
gasErrors={gasErrors}
gasWarnings={gasWarnings}
onManualChange={onManualChange}

View File

@ -9,6 +9,7 @@ import TransactionBreakdown from '../transaction-breakdown';
import Button from '../../ui/button';
import Tooltip from '../../ui/tooltip';
import Copy from '../../ui/icon/copy-icon.component';
import CancelButton from '../cancel-button';
import Popover from '../../ui/popover';
import { SECOND } from '../../../../shared/constants/time';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
@ -32,7 +33,6 @@ export default class TransactionListItemDetails extends PureComponent {
showSpeedUp: PropTypes.bool,
showRetry: PropTypes.bool,
isEarliestNonce: PropTypes.bool,
cancelDisabled: PropTypes.bool,
primaryCurrency: PropTypes.string,
transactionGroup: PropTypes.object,
title: PropTypes.string.isRequired,
@ -114,38 +114,6 @@ export default class TransactionListItemDetails extends PureComponent {
}
}
renderCancel() {
const { t } = this.context;
const { showCancel, cancelDisabled } = this.props;
if (!showCancel) {
return null;
}
return cancelDisabled ? (
<Tooltip title={t('notEnoughGas')} position="bottom">
<div>
<Button
type="raised"
onClick={this.handleCancel}
className="transaction-list-item-details__header-button"
disabled
>
{t('cancel')}
</Button>
</div>
</Tooltip>
) : (
<Button
type="raised"
onClick={this.handleCancel}
className="transaction-list-item-details__header-button"
>
{t('cancel')}
</Button>
);
}
render() {
const { t } = this.context;
const { justCopied } = this.state;
@ -163,6 +131,7 @@ export default class TransactionListItemDetails extends PureComponent {
title,
onClose,
recipientNickname,
showCancel,
} = this.props;
const {
primaryTransaction: transaction,
@ -185,7 +154,13 @@ export default class TransactionListItemDetails extends PureComponent {
{t('speedUp')}
</Button>
)}
{this.renderCancel()}
{showCancel && (
<CancelButton
transaction={transaction}
cancelTransaction={this.handleCancel}
detailsModal
/>
)}
<Tooltip
wrapperClassName="transaction-list-item-details__header-button"
containerClassName="transaction-list-item-details__header-button-tooltip-container"

View File

@ -5,10 +5,7 @@ import { useHistory } from 'react-router-dom';
import ListItem from '../../ui/list-item';
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useCancelTransaction } from '../../../hooks/useCancelTransaction';
import { useRetryTransaction } from '../../../hooks/useRetryTransaction';
import Button from '../../ui/button';
import Tooltip from '../../ui/tooltip';
import TransactionListItemDetails from '../transaction-list-item-details';
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes';
import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp';
@ -20,6 +17,9 @@ import {
} from '../../../../shared/constants/transaction';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import EditGasPopover from '../edit-gas-popover';
import { useMetricEvent } from '../../../hooks/useMetricEvent';
import Button from '../../ui/button';
import CancelButton from '../cancel-button';
export default function TransactionListItem({
transactionGroup,
@ -29,24 +29,50 @@ export default function TransactionListItem({
const history = useHistory();
const { hasCancelled } = transactionGroup;
const [showDetails, setShowDetails] = useState(false);
const [showCancelEditGasPopover, setShowCancelEditGasPopover] = useState(
false,
);
const [showRetryEditGasPopover, setShowRetryEditGasPopover] = useState(false);
const {
initialTransaction: { id },
primaryTransaction: { err, status },
} = transactionGroup;
const {
hasEnoughCancelGas,
cancelTransaction,
showCancelEditGasPopover,
closeCancelEditGasPopover,
customCancelGasSettings,
} = useCancelTransaction(transactionGroup);
const {
retryTransaction,
showRetryEditGasPopover,
closeRetryEditGasPopover,
customRetryGasSettings,
} = useRetryTransaction(transactionGroup);
const speedUpMetricsEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Clicked "Speed Up"',
},
});
const cancelMetricsEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Clicked "Cancel"',
},
});
const retryTransaction = useCallback(
async (event) => {
event.stopPropagation();
setShowRetryEditGasPopover(true);
speedUpMetricsEvent();
},
[speedUpMetricsEvent],
);
const cancelTransaction = useCallback(
(event) => {
event.stopPropagation();
setShowCancelEditGasPopover(true);
cancelMetricsEvent();
},
[cancelMetricsEvent],
);
const shouldShowSpeedUp = useShouldShowSpeedUp(
transactionGroup,
isEarliestNonce,
@ -90,37 +116,6 @@ export default function TransactionListItem({
setShowDetails((prev) => !prev);
}, [isUnapproved, history, id]);
const cancelButton = useMemo(() => {
const btn = (
<Button
onClick={cancelTransaction}
rounded
className="transaction-list-item__header-button"
disabled={!hasEnoughCancelGas}
>
{t('cancel')}
</Button>
);
if (hasCancelled || !isPending || isUnapproved) {
return null;
}
return hasEnoughCancelGas ? (
btn
) : (
<Tooltip title={t('notEnoughGas')} position="bottom">
<div>{btn}</div>
</Tooltip>
);
}, [
isPending,
t,
isUnapproved,
hasEnoughCancelGas,
cancelTransaction,
hasCancelled,
]);
const speedUpButton = useMemo(() => {
if (!shouldShowSpeedUp || !isPending || isUnapproved) {
return null;
@ -140,11 +135,13 @@ export default function TransactionListItem({
isUnapproved,
t,
isPending,
retryTransaction,
hasCancelled,
retryTransaction,
cancelTransaction,
]);
const showCancelButton = !hasCancelled && isPending && !isUnapproved;
return (
<>
<ListItem
@ -194,7 +191,12 @@ export default function TransactionListItem({
>
<div className="transaction-list-item__pending-actions">
{speedUpButton}
{cancelButton}
{showCancelButton && (
<CancelButton
transaction={transactionGroup.primaryTransaction}
cancelTransaction={cancelTransaction}
/>
)}
</div>
</ListItem>
{showDetails && (
@ -211,35 +213,20 @@ export default function TransactionListItem({
isEarliestNonce={isEarliestNonce}
onCancel={cancelTransaction}
showCancel={isPending && !hasCancelled}
cancelDisabled={!hasEnoughCancelGas}
/>
)}
{showRetryEditGasPopover && (
<EditGasPopover
onClose={closeRetryEditGasPopover}
onClose={() => setShowRetryEditGasPopover(false)}
mode={EDIT_GAS_MODES.SPEED_UP}
transaction={{
...transactionGroup.primaryTransaction,
userFeeLevel: 'custom',
txParams: {
...transactionGroup.primaryTransaction?.txParams,
...customRetryGasSettings,
},
}}
transaction={transactionGroup.primaryTransaction}
/>
)}
{showCancelEditGasPopover && (
<EditGasPopover
onClose={closeCancelEditGasPopover}
onClose={() => setShowCancelEditGasPopover(false)}
mode={EDIT_GAS_MODES.CANCEL}
transaction={{
...transactionGroup.primaryTransaction,
userFeeLevel: 'custom',
txParams: {
...transactionGroup.primaryTransaction?.txParams,
...customCancelGasSettings,
},
}}
transaction={transactionGroup.primaryTransaction}
/>
)}
</>

View File

@ -0,0 +1,139 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { fireEvent } from '@testing-library/react';
import transactionGroup from '../../../../test/data/mock-pending-transaction-data.json';
import {
getConversionRate,
getSelectedAccount,
getTokenExchangeRates,
getPreferences,
getShouldShowFiat,
} from '../../../selectors';
import {
renderWithProvider,
setBackgroundConnection,
} from '../../../../test/jest';
import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import TransactionListItem from './transaction-list-item.component';
const FEE_MARKET_ESTIMATE_RETURN_VALUE = {
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
gasFeeEstimates: {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '53',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '7',
suggestedMaxFeePerGas: '70',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '10',
suggestedMaxFeePerGas: '100',
},
estimatedBaseFee: '50',
},
estimatedGasFeeTimeBounds: {},
};
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useSelector: jest.fn(),
useDispatch: jest.fn(),
};
});
jest.mock('../../../hooks/useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
setBackgroundConnection({
getGasFeeTimeEstimate: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
});
const generateUseSelectorRouter = (opts) => (selector) => {
if (selector === getConversionRate) {
return 1;
} else if (selector === getSelectedAccount) {
return {
balance: opts.balance ?? '2AA1EFB94E0000',
};
} else if (selector === getTokenExchangeRates) {
return opts.tokenExchangeRates ?? {};
} else if (selector === getPreferences) {
return (
opts.preferences ?? {
useNativeCurrencyAsPrimaryCurrency: true,
}
);
} else if (selector === getShouldShowFiat) {
return opts.shouldShowFiat ?? false;
}
return undefined;
};
describe('TransactionListItem', () => {
describe('when account has insufficient balance to cover gas', function () {
beforeAll(function () {
useGasFeeEstimates.mockImplementation(
() => FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
});
afterAll(function () {
useGasFeeEstimates.restore();
});
it(`should indicate account has insufficient funds to cover gas price for cancellation of pending transaction`, function () {
useSelector.mockImplementation(
generateUseSelectorRouter({
balance: '0x3',
}),
);
const { queryByTestId } = renderWithProvider(
<TransactionListItem transactionGroup={transactionGroup} />,
);
expect(queryByTestId('not-enough-gas__tooltip')).toBeInTheDocument();
});
it('should not disable "cancel" button when user has sufficient funds', function () {
useSelector.mockImplementation(
generateUseSelectorRouter({
balance: '2AA1EFB94E0000',
}),
);
const { queryByTestId } = renderWithProvider(
<TransactionListItem transactionGroup={transactionGroup} />,
);
expect(queryByTestId('not-enough-gas__tooltip')).not.toBeInTheDocument();
});
it(`should open the edit gas popover when cancel is clicked`, function () {
useSelector.mockImplementation(
generateUseSelectorRouter({
balance: '2AA1EFB94E0000',
}),
);
const { getByText, queryByText } = renderWithProvider(
<TransactionListItem transactionGroup={transactionGroup} />,
);
expect(queryByText('Cancel transaction')).not.toBeInTheDocument();
const cancelButton = getByText('Cancel');
fireEvent.click(cancelButton);
expect(getByText('Cancel transaction')).toBeInTheDocument();
});
});
});

View File

@ -1,55 +0,0 @@
import { useSelector } from 'react-redux';
import { useCallback, useState } from 'react';
import { isBalanceSufficient } from '../pages/send/send.utils';
import { getSelectedAccount } from '../selectors';
import { getConversionRate } from '../ducks/metamask/metamask';
import { getMaximumGasTotalInHexWei } from '../../shared/modules/gas.utils';
import { useIncrementedGasFees } from './useIncrementedGasFees';
/**
* Determine whether a transaction can be cancelled and provide a method to
* kick off the process of cancellation.
*
* Provides a reusable hook that, given a transactionGroup, will return
* whether or not the account has enough funds to cover the gas cancellation
* fee, and a method for beginning the cancellation process
* @param {Object} transactionGroup
* @return {[boolean, Function]}
*/
export function useCancelTransaction(transactionGroup) {
const { primaryTransaction } = transactionGroup;
const customCancelGasSettings = useIncrementedGasFees(transactionGroup);
const selectedAccount = useSelector(getSelectedAccount);
const conversionRate = useSelector(getConversionRate);
const [showCancelEditGasPopover, setShowCancelEditGasPopover] = useState(
false,
);
const closeCancelEditGasPopover = () => setShowCancelEditGasPopover(false);
const cancelTransaction = useCallback((event) => {
event.stopPropagation();
return setShowCancelEditGasPopover(true);
}, []);
const hasEnoughCancelGas =
primaryTransaction.txParams &&
isBalanceSufficient({
amount: '0x0',
gasTotal: getMaximumGasTotalInHexWei(customCancelGasSettings),
balance: selectedAccount.balance,
conversionRate,
});
return {
hasEnoughCancelGas,
customCancelGasSettings,
cancelTransaction,
showCancelEditGasPopover,
closeCancelEditGasPopover,
};
}

View File

@ -1,114 +0,0 @@
import * as reactRedux from 'react-redux';
import { renderHook } from '@testing-library/react-hooks';
import sinon from 'sinon';
import transactions from '../../test/data/transaction-data.json';
import { getConversionRate, getSelectedAccount } from '../selectors';
import { increaseLastGasPrice } from '../helpers/utils/confirm-tx.util';
import { useCancelTransaction } from './useCancelTransaction';
jest.mock('../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
}));
describe('useCancelTransaction', function () {
let useSelector;
const dispatch = sinon.spy();
beforeAll(function () {
sinon.stub(reactRedux, 'useDispatch').returns(dispatch);
});
afterEach(function () {
dispatch.resetHistory();
});
afterAll(function () {
sinon.restore();
});
describe('when account has insufficient balance to cover gas', function () {
beforeAll(function () {
useSelector = sinon.stub(reactRedux, 'useSelector');
useSelector.callsFake((selector) => {
if (selector === getConversionRate) {
return 280.46;
} else if (selector === getSelectedAccount) {
return {
balance: '0x3',
};
}
return undefined;
});
});
afterAll(function () {
useSelector.restore();
});
transactions.forEach((transactionGroup) => {
const originalGasPrice =
transactionGroup.primaryTransaction.txParams?.gasPrice;
const gasPrice =
originalGasPrice && increaseLastGasPrice(originalGasPrice);
const transactionId = transactionGroup.initialTransaction.id;
it(`should indicate account has insufficient funds to cover ${gasPrice} gas price`, function () {
const { result } = renderHook(() =>
useCancelTransaction(transactionGroup),
);
expect(result.current.hasEnoughCancelGas).toStrictEqual(false);
});
it(`should return a function that kicks off cancellation for id ${transactionId}`, function () {
const { result } = renderHook(() =>
useCancelTransaction(transactionGroup),
);
expect(typeof result.current.cancelTransaction).toStrictEqual(
'function',
);
});
});
});
describe('when account has sufficient balance to cover gas', function () {
beforeAll(function () {
useSelector = sinon.stub(reactRedux, 'useSelector');
useSelector.callsFake((selector) => {
if (selector === getConversionRate) {
return 280.46;
} else if (selector === getSelectedAccount) {
return {
balance: '0x9C2007651B2500000',
};
}
return undefined;
});
});
afterAll(function () {
useSelector.restore();
});
transactions.forEach((transactionGroup) => {
const originalGasPrice =
transactionGroup.primaryTransaction.txParams?.gasPrice;
const gasPrice =
originalGasPrice && increaseLastGasPrice(originalGasPrice);
const transactionId = transactionGroup.initialTransaction.id;
it(`should indicate account has funds to cover ${gasPrice} gas price`, function () {
const { result } = renderHook(() =>
useCancelTransaction(transactionGroup),
);
expect(result.current.hasEnoughCancelGas).toStrictEqual(true);
});
it(`should return a function that opens the gas popover onsubmit kicks off cancellation for id ${transactionId}`, function () {
const { result } = renderHook(() =>
useCancelTransaction(transactionGroup),
);
expect(typeof result.current.cancelTransaction).toStrictEqual(
'function',
);
});
});
});
});

View File

@ -50,14 +50,12 @@ function getHighestIncrementedFee(originalFee, currentEstimate) {
* discarded by the network to avoid DoS attacks. This hook returns an object
* that either has gasPrice or maxFeePerGas/maxPriorityFeePerGas specified. In
* addition the gasLimit will also be included.
* @param {} transactionGroup
* @param {} transaction
* @returns {import(
* '../../app/scripts/controllers/transactions'
* ).CustomGasSettings} - Gas settings for cancellations/speed ups
*/
export function useIncrementedGasFees(transactionGroup) {
const { primaryTransaction } = transactionGroup;
export function useIncrementedGasFees(transaction) {
const { gasFeeEstimates = {} } = useGasFeeEstimates();
// We memoize this value so that it can be relied upon in other hooks.
@ -68,8 +66,8 @@ export function useIncrementedGasFees(transactionGroup) {
// do not have txParams. This is why we use optional chaining on the
// txParams object in this hook.
const temporaryGasSettings = {
gasLimit: primaryTransaction.txParams?.gas,
gas: primaryTransaction.txParams?.gas,
gasLimit: transaction.txParams?.gas,
gas: transaction.txParams?.gas,
};
const suggestedMaxFeePerGas =
@ -77,10 +75,10 @@ export function useIncrementedGasFees(transactionGroup) {
const suggestedMaxPriorityFeePerGas =
gasFeeEstimates?.medium?.suggestedMaxPriorityFeePerGas ?? '0';
if (isEIP1559Transaction(primaryTransaction)) {
const transactionMaxFeePerGas = primaryTransaction.txParams?.maxFeePerGas;
if (isEIP1559Transaction(transaction)) {
const transactionMaxFeePerGas = transaction.txParams?.maxFeePerGas;
const transactionMaxPriorityFeePerGas =
primaryTransaction.txParams?.maxPriorityFeePerGas;
transaction.txParams?.maxPriorityFeePerGas;
temporaryGasSettings.maxFeePerGas =
transactionMaxFeePerGas === undefined ||
@ -99,7 +97,7 @@ export function useIncrementedGasFees(transactionGroup) {
suggestedMaxPriorityFeePerGas,
);
} else {
const transactionGasPrice = primaryTransaction.txParams?.gasPrice;
const transactionGasPrice = transaction.txParams?.gasPrice;
temporaryGasSettings.gasPrice =
transactionGasPrice === undefined || transactionGasPrice.startsWith('-')
? '0x0'
@ -109,7 +107,7 @@ export function useIncrementedGasFees(transactionGroup) {
);
}
return temporaryGasSettings;
}, [primaryTransaction, gasFeeEstimates]);
}, [transaction, gasFeeEstimates]);
return customGasSettings;
}

View File

@ -1,48 +0,0 @@
import { useCallback, useState } from 'react';
import { useMetricEvent } from './useMetricEvent';
import { useIncrementedGasFees } from './useIncrementedGasFees';
/**
* @typedef {Object} RetryTransactionReturnValue
* @property {(event: Event) => void} retryTransaction - open edit gas popover
* to begin setting retry gas fees
* @property {boolean} showRetryEditGasPopover - Whether to show the popover
* @property {() => void} closeRetryEditGasPopover - close the popover.
*/
/**
* Provides a reusable hook that, given a transactionGroup, will return
* a method for beginning the retry process
* @param {Object} transactionGroup - the transaction group
* @return {RetryTransactionReturnValue}
*/
export function useRetryTransaction(transactionGroup) {
const customRetryGasSettings = useIncrementedGasFees(transactionGroup);
const trackMetricsEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Clicked "Speed Up"',
},
});
const [showRetryEditGasPopover, setShowRetryEditGasPopover] = useState(false);
const closeRetryEditGasPopover = () => setShowRetryEditGasPopover(false);
const retryTransaction = useCallback(
async (event) => {
event.stopPropagation();
setShowRetryEditGasPopover(true);
trackMetricsEvent();
},
[trackMetricsEvent],
);
return {
retryTransaction,
showRetryEditGasPopover,
closeRetryEditGasPopover,
customRetryGasSettings,
};
}

View File

@ -1,77 +0,0 @@
import * as reactRedux from 'react-redux';
import { renderHook, act } from '@testing-library/react-hooks';
import sinon from 'sinon';
import transactions from '../../test/data/transaction-data.json';
import { getIsMainnet } from '../selectors';
import * as methodDataHook from './useMethodData';
import * as metricEventHook from './useMetricEvent';
import { useRetryTransaction } from './useRetryTransaction';
jest.mock('./useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn().mockImplementation(() => Promise.resolve({})),
}));
describe('useRetryTransaction', () => {
describe('when transaction meets retry enabled criteria', () => {
let useSelector;
const dispatch = sinon.spy(() => Promise.resolve({ blockTime: 0 }));
const trackEvent = sinon.spy();
const event = {
preventDefault: () => undefined,
stopPropagation: () => undefined,
};
beforeAll(() => {
sinon.stub(reactRedux, 'useDispatch').returns(dispatch);
sinon.stub(methodDataHook, 'useMethodData').returns({});
sinon.stub(metricEventHook, 'useMetricEvent').returns(trackEvent);
useSelector = sinon.stub(reactRedux, 'useSelector');
useSelector.callsFake((selector) => {
if (selector === getIsMainnet) {
return true;
}
return undefined;
});
});
afterEach(() => {
dispatch.resetHistory();
trackEvent.resetHistory();
});
afterAll(() => {
sinon.restore();
});
const retryEnabledTransaction = {
...transactions[0],
transactions: [
{
submittedTime: new Date() - 5001,
},
],
hasRetried: false,
};
it('retryTransaction function should track metrics', () => {
const { result } = renderHook(() =>
useRetryTransaction(retryEnabledTransaction, true),
);
const { retryTransaction } = result.current;
act(() => {
retryTransaction(event);
});
expect(trackEvent.calledOnce).toStrictEqual(true);
});
it('retryTransaction function should show retry popover', async () => {
const { result } = renderHook(() =>
useRetryTransaction(retryEnabledTransaction, true),
);
const { retryTransaction } = result.current;
await act(async () => {
await retryTransaction(event);
});
expect(result.current.showRetryEditGasPopover).toStrictEqual(true);
});
});
});