1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

EIP-1559 and Rinkeby Testnet support in Swaps (#11635)

This commit is contained in:
Daniel 2021-07-30 13:35:30 +02:00 committed by GitHub
parent cbfde8a080
commit 714170c7b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1037 additions and 151 deletions

View File

@ -651,7 +651,7 @@
"message": "High"
},
"editGasLimitOutOfBounds": {
"message": "Gas limit must be greater than 20999 and less than 7920027"
"message": "Gas limit must be at least $1"
},
"editGasLimitTooltip": {
"message": "Gas limit is the maximum units of gas you are willing to use. Units of gas are a multiplier to “Max priority fee” and “Max fee”."
@ -1337,6 +1337,9 @@
"networkNamePolygon": {
"message": "Polygon"
},
"networkNameRinkeby": {
"message": "Rinkeby"
},
"networkNameTestnet": {
"message": "Testnet"
},
@ -2146,9 +2149,19 @@
"message": "The swap of $1 to $2",
"description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token"
},
"swapGasFeesDetails": {
"message": "Gas fees are estimated and will fluctuate based on network traffic and transaction complexity."
},
"swapGasFeesLearnMore": {
"message": "Learn more about gas fees"
},
"swapGasFeesSplit": {
"message": "Gas fees on the previous screen are split between these two transactions."
},
"swapGasFeesSummary": {
"message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.",
"description": "$1 is the selected network, e.g. Ethereum or BSC"
},
"swapHighSlippageWarning": {
"message": "Slippage amount is very high."
},

View File

@ -6,7 +6,11 @@ import { mapValues, cloneDeep } from 'lodash';
import abi from 'human-standard-token-abi';
import { calcTokenAmount } from '../../../ui/helpers/utils/token-util';
import { calcGasTotal } from '../../../ui/pages/send/send.utils';
import { conversionUtil } from '../../../shared/modules/conversion.utils';
import {
conversionUtil,
decGWEIToHexWEI,
addCurrencies,
} from '../../../shared/modules/conversion.utils';
import {
DEFAULT_ERC20_APPROVE_GAS,
QUOTES_EXPIRED_ERROR,
@ -14,6 +18,7 @@ import {
SWAPS_FETCH_ORDER_CONFLICT,
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
} from '../../../shared/constants/swaps';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils';
@ -66,6 +71,8 @@ const initialState = {
quotesLastFetched: null,
customMaxGas: '',
customGasPrice: null,
customMaxFeePerGas: null,
customMaxPriorityFeePerGas: null,
selectedAggId: null,
customApproveTxData: '',
errorKey: '',
@ -87,6 +94,7 @@ export default class SwapsController {
fetchTradesInfo = defaultFetchTradesInfo,
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
getCurrentChainId,
getEIP1559GasFeeEstimates,
}) {
this.store = new ObservableStore({
swapsState: { ...initialState.swapsState },
@ -95,6 +103,7 @@ export default class SwapsController {
this._fetchTradesInfo = fetchTradesInfo;
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime;
this._getCurrentChainId = getCurrentChainId;
this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
this.getBufferedGasLimit = getBufferedGasLimit;
this.tokenRatesStore = tokenRatesStore;
@ -440,6 +449,23 @@ export default class SwapsController {
});
}
setSwapsTxMaxFeePerGas(maxFeePerGas) {
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: { ...swapsState, customMaxFeePerGas: maxFeePerGas },
});
}
setSwapsTxMaxFeePriorityPerGas(maxPriorityFeePerGas) {
const { swapsState } = this.store.getState();
this.store.updateState({
swapsState: {
...swapsState,
customMaxPriorityFeePerGas: maxPriorityFeePerGas,
},
});
}
setSwapsTxGasLimit(gasLimit) {
const { swapsState } = this.store.getState();
this.store.updateState({
@ -494,16 +520,11 @@ export default class SwapsController {
clearTimeout(this.pollingTimeout);
}
async _getEthersGasPrice() {
const ethersGasPrice = await this.ethersProvider.getGasPrice();
return ethersGasPrice.toHexString();
}
async _findTopQuoteAndCalculateSavings(quotes = {}) {
const tokenConversionRates = this.tokenRatesStore.getState()
.contractExchangeRates;
const {
swapsState: { customGasPrice },
swapsState: { customGasPrice, customMaxPriorityFeePerGas },
} = this.store.getState();
const chainId = this._getCurrentChainId();
@ -514,7 +535,36 @@ export default class SwapsController {
const newQuotes = cloneDeep(quotes);
const usedGasPrice = customGasPrice || (await this._getEthersGasPrice());
const {
gasFeeEstimates,
gasEstimateType,
} = await this._getEIP1559GasFeeEstimates();
let usedGasPrice = '0x0';
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
const {
high: { suggestedMaxPriorityFeePerGas },
estimatedBaseFee,
} = gasFeeEstimates;
usedGasPrice = addCurrencies(
customMaxPriorityFeePerGas || // Is already in hex WEI.
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
decGWEIToHexWEI(estimatedBaseFee),
{
aBase: 16,
bBase: 16,
toNumericBase: 'hex',
numberOfDecimals: 6,
},
);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
usedGasPrice = customGasPrice || decGWEIToHexWEI(gasFeeEstimates.high);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
usedGasPrice =
customGasPrice || decGWEIToHexWEI(gasFeeEstimates.gasPrice);
}
let topAggId = null;
let overallValueOfBestQuoteForSorting = null;

View File

@ -13,6 +13,7 @@ import {
import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps';
import { createTestProviderTools } from '../../../test/stub/provider';
import { SECOND } from '../../../shared/constants/time';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import SwapsController, { utils } from './swaps';
import { NETWORK_EVENTS } from './network';
@ -120,7 +121,9 @@ const EMPTY_INIT_STATE = {
tradeTxId: null,
approveTxId: null,
quotesLastFetched: null,
customMaxFeePerGas: null,
customMaxGas: '',
customMaxPriorityFeePerGas: null,
customGasPrice: null,
selectedAggId: null,
customApproveTxData: '',
@ -138,6 +141,14 @@ const fetchTradesInfoStub = sandbox.stub();
const fetchSwapsQuoteRefreshTimeStub = sandbox.stub();
const getCurrentChainIdStub = sandbox.stub();
getCurrentChainIdStub.returns(MAINNET_CHAIN_ID);
const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => {
return {
gasFeeEstimates: {
high: '150',
},
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
};
});
describe('SwapsController', function () {
let provider;
@ -152,6 +163,7 @@ describe('SwapsController', function () {
fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
getCurrentChainId: getCurrentChainIdStub,
getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub,
});
};
@ -687,8 +699,8 @@ describe('SwapsController', function () {
total: '5.4949494949494949495',
medianMetaMaskFee: '0.44444444444444444444',
},
ethFee: '33554432',
overallValueOfQuote: '-33554382',
ethFee: '5.033165',
overallValueOfQuote: '44.966835',
metaMaskFeeInEth: '0.5050505050505050505',
ethValueOfTokens: '50',
});

View File

@ -1200,10 +1200,12 @@ export default class TransactionController extends EventEmitter {
txMeta.chainId,
);
const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10)
.div(txMeta.swapMetaData.token_to_amount, 10)
.times(100)
.round(2)}%`;
const quoteVsExecutionRatio = tokensReceived
? `${new BigNumber(tokensReceived, 10)
.div(txMeta.swapMetaData.token_to_amount, 10)
.times(100)
.round(2)}%`
: null;
const estimatedVsUsedGasRatio = `${new BigNumber(
txMeta.txReceipt.gasUsed,

View File

@ -498,6 +498,9 @@ export default class MetamaskController extends EventEmitter {
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind(
this.gasFeeController,
),
});
// ensure accountTracker updates balances after network change
@ -1042,6 +1045,14 @@ export default class MetamaskController extends EventEmitter {
swapsController.setSwapsTxGasLimit,
swapsController,
),
setSwapsTxMaxFeePerGas: nodeify(
swapsController.setSwapsTxMaxFeePerGas,
swapsController,
),
setSwapsTxMaxFeePriorityPerGas: nodeify(
swapsController.setSwapsTxMaxFeePriorityPerGas,
swapsController,
),
safeRefetchQuotes: nodeify(
swapsController.safeRefetchQuotes,
swapsController,

View File

@ -37,4 +37,5 @@ export const EDIT_GAS_MODES = {
SPEED_UP: 'speed-up',
CANCEL: 'cancel',
MODIFY_IN_PLACE: 'modify-in-place',
SWAPS: 'swaps',
};

View File

@ -9,6 +9,7 @@ import {
POLYGON_CHAIN_ID,
MATIC_SYMBOL,
MATIC_TOKEN_IMAGE_URL,
RINKEBY_CHAIN_ID,
} from './network';
export const QUOTES_EXPIRED_ERROR = 'quotes-expired';
@ -54,6 +55,14 @@ export const TEST_ETH_SWAPS_TOKEN_OBJECT = {
iconUrl: TEST_ETH_TOKEN_IMAGE_URL,
};
export const RINKEBY_SWAPS_TOKEN_OBJECT = {
symbol: ETH_SYMBOL,
name: 'Ether',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 18,
iconUrl: TEST_ETH_TOKEN_IMAGE_URL,
};
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0';
@ -78,6 +87,7 @@ const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network';
const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/';
const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
const RINKEBY_DEFAULT_BLOCK_EXPLORER_URL = 'https://rinkeby.etherscan.io/';
const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/';
export const ALLOWED_SWAPS_CHAIN_IDS = {
@ -85,6 +95,7 @@ export const ALLOWED_SWAPS_CHAIN_IDS = {
[SWAPS_TESTNET_CHAIN_ID]: true,
[BSC_CHAIN_ID]: true,
[POLYGON_CHAIN_ID]: true,
[RINKEBY_CHAIN_ID]: true,
};
// This is mapping for v1 URLs and will be removed once we migrate to v2.
@ -92,6 +103,7 @@ export const METASWAP_CHAINID_API_HOST_MAP = {
[MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST,
[SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST,
[BSC_CHAIN_ID]: METASWAP_BSC_API_HOST,
[RINKEBY_CHAIN_ID]: SWAPS_TESTNET_HOST,
};
export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
@ -99,6 +111,7 @@ export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
[SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS,
[BSC_CHAIN_ID]: BSC_CONTRACT_ADDRESS,
[POLYGON_CHAIN_ID]: POLYGON_CONTRACT_ADDRESS,
[RINKEBY_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS,
};
export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
@ -106,14 +119,17 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
[SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT,
[BSC_CHAIN_ID]: BNB_SWAPS_TOKEN_OBJECT,
[POLYGON_CHAIN_ID]: MATIC_SWAPS_TOKEN_OBJECT,
[RINKEBY_CHAIN_ID]: RINKEBY_SWAPS_TOKEN_OBJECT,
};
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
[MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL,
[POLYGON_CHAIN_ID]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL,
[RINKEBY_CHAIN_ID]: RINKEBY_DEFAULT_BLOCK_EXPLORER_URL,
};
export const ETHEREUM = 'ethereum';
export const POLYGON = 'polygon';
export const BSC = 'bsc';
export const RINKEBY = 'rinkeby';

View File

@ -268,6 +268,15 @@ const toNegative = (n, options = {}) => {
return multiplyCurrencies(n, -1, options);
};
export function decGWEIToHexWEI(decGWEI) {
return conversionUtil(decGWEI, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
fromDenomination: 'GWEI',
toDenomination: 'WEI',
});
}
export {
conversionUtil,
addCurrencies,

View File

@ -217,7 +217,7 @@ export const createSwapsMockStore = () => {
selectedAggId: 'TEST_AGG_2',
customApproveTxData: '',
errorKey: '',
topAggId: null,
topAggId: 'TEST_AGG_BEST',
routeState: '',
swapsFeatureIsLive: false,
useNewSwapsApi: false,

View File

@ -79,3 +79,27 @@ export const createFeatureFlagsResponse = () => {
},
};
};
export const createGasFeeEstimatesForFeeMarket = () => {
return {
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',
};
};

View File

@ -33,6 +33,7 @@ export default function AdvancedGasControls({
maxPriorityFeeFiat,
maxFeeFiat,
gasErrors,
minimumGasLimit = 21000,
}) {
const t = useContext(I18nContext);
@ -65,7 +66,7 @@ export default function AdvancedGasControls({
titleText={t('gasLimit')}
error={
gasErrors?.gasLimit
? getGasFormErrorText(gasErrors.gasLimit, t)
? getGasFormErrorText(gasErrors.gasLimit, t, { minimumGasLimit })
: null
}
onChange={setGasLimit}
@ -231,4 +232,5 @@ AdvancedGasControls.propTypes = {
maxPriorityFeeFiat: PropTypes.string,
maxFeeFiat: PropTypes.string,
gasErrors: PropTypes.object,
minimumGasLimit: PropTypes.number,
};

View File

@ -60,6 +60,7 @@ export default function EditGasDisplay({
warning,
gasErrors,
onManualChange,
minimumGasLimit,
}) {
const t = useContext(I18nContext);
@ -218,6 +219,7 @@ export default function EditGasDisplay({
maxFeeFiat={maxFeePerGasFiat}
gasErrors={gasErrors}
onManualChange={onManualChange}
minimumGasLimit={minimumGasLimit}
/>
)}
</div>
@ -266,4 +268,5 @@ EditGasDisplay.propTypes = {
transaction: PropTypes.object,
gasErrors: PropTypes.object,
onManualChange: PropTypes.func,
minimumGasLimit: PropTypes.number,
};

View File

@ -13,6 +13,7 @@ import {
import {
decGWEIToHexWEI,
decimalToHex,
hexToDecimal,
} from '../../../helpers/utils/conversions.util';
import Popover from '../../ui/popover';
@ -27,6 +28,7 @@ import {
hideModal,
hideSidebar,
updateTransaction,
updateCustomSwapsEIP1559GasParams,
} from '../../../store/actions';
import LoadingHeartBeat from '../../ui/loading-heartbeat';
@ -38,6 +40,7 @@ export default function EditGasPopover({
transaction,
mode,
onClose,
minimumGasLimit,
}) {
const t = useContext(I18nContext);
const dispatch = useDispatch();
@ -56,6 +59,8 @@ export default function EditGasPopover({
setDappSuggestedGasFeeAcknowledged,
] = useState(false);
const minimumGasLimitDec = hexToDecimal(minimumGasLimit);
const {
maxPriorityFeePerGas,
setMaxPriorityFeePerGas,
@ -78,7 +83,7 @@ export default function EditGasPopover({
hasGasErrors,
gasErrors,
onManualChange,
} = useGasFeeInputs(defaultEstimateToUse, transaction);
} = useGasFeeInputs(defaultEstimateToUse, transaction, minimumGasLimit, mode);
const [showAdvancedForm, setShowAdvancedForm] = useState(
!estimateToUse || hasGasErrors,
@ -137,6 +142,12 @@ export default function EditGasPopover({
}),
);
break;
case EDIT_GAS_MODES.SWAPS:
// This popover component should only be used for the "FEE_MARKET" type in Swaps.
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
dispatch(updateCustomSwapsEIP1559GasParams(newGasSettings));
}
break;
default:
break;
}
@ -227,6 +238,7 @@ export default function EditGasPopover({
hasGasErrors={hasGasErrors}
gasErrors={gasErrors}
onManualChange={onManualChange}
minimumGasLimit={minimumGasLimitDec}
{...editGasDisplayProps}
/>
</>
@ -244,4 +256,5 @@ EditGasPopover.propTypes = {
transaction: PropTypes.object,
mode: PropTypes.oneOf(Object.values(EDIT_GAS_MODES)),
defaultEstimateToUse: PropTypes.string,
minimumGasLimit: PropTypes.string,
};

View File

@ -22,14 +22,14 @@ export default function TransactionDetailItem({
<Typography
color={detailTitleColor}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H6}
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__title"
>
{detailTitle}
</Typography>
{detailText && (
<Typography
variant={TYPOGRAPHY.H6}
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__detail-text"
color={COLORS.UI4}
>
@ -39,20 +39,24 @@ export default function TransactionDetailItem({
<Typography
color={COLORS.BLACK}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H6}
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__total"
>
{detailTotal}
</Typography>
</div>
<div className="transaction-detail-item__row">
<Typography
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__subtitle"
color={COLORS.UI4}
>
{subTitle}
</Typography>
{React.isValidElement(subTitle) ? (
<div className="transaction-detail-item__subtitle">{subTitle}</div>
) : (
<Typography
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__subtitle"
color={COLORS.UI4}
>
{subTitle}
</Typography>
)}
<Typography
variant={TYPOGRAPHY.H7}

View File

@ -24,6 +24,10 @@
padding: 20px 0;
border-bottom: 1px solid $ui-3;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
border-bottom: 0;

View File

@ -285,7 +285,7 @@ export function getUnapprovedTxs(state) {
}
export function isEIP1559Network(state) {
return state.metamask.networkDetails.EIPS[1559] === true;
return state.metamask.networkDetails?.EIPS[1559] === true;
}
export function getGasEstimateType(state) {

View File

@ -45,6 +45,7 @@ import {
getValueFromWeiHex,
decGWEIToHexWEI,
hexWEIToDecGWEI,
addHexes,
} from '../../helpers/utils/conversions.util';
import { conversionLessThan } from '../../../shared/modules/conversion.utils';
import { calcTokenAmount } from '../../helpers/utils/token-util';
@ -65,6 +66,7 @@ import {
SWAPS_FETCH_ORDER_CONFLICT,
} from '../../../shared/constants/swaps';
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
import { isEIP1559Network, getGasFeeEstimates } from '../metamask/metamask';
const GAS_PRICES_LOADING_STATES = {
INITIAL: 'INITIAL',
@ -242,6 +244,12 @@ export const getCustomSwapsGas = (state) =>
export const getCustomSwapsGasPrice = (state) =>
state.metamask.swapsState.customGasPrice;
export const getCustomMaxFeePerGas = (state) =>
state.metamask.swapsState.customMaxFeePerGas;
export const getCustomMaxPriorityFeePerGas = (state) =>
state.metamask.swapsState.customMaxPriorityFeePerGas;
export const getFetchParams = (state) => state.metamask.swapsState.fetchParams;
export const getQuotes = (state) => state.metamask.swapsState.quotes;
@ -503,6 +511,7 @@ export const fetchQuotesAndSetQuoteState = (
const hardwareWalletUsed = isHardwareWallet(state);
const hardwareWalletType = getHardwareWalletType(state);
const EIP1559Network = isEIP1559Network(state);
metaMetricsEvent({
event: 'Quotes Requested',
category: 'swaps',
@ -544,7 +553,9 @@ export const fetchQuotesAndSetQuoteState = (
),
);
const gasPriceFetchPromise = dispatch(fetchAndSetSwapsGasPriceInfo());
const gasPriceFetchPromise = EIP1559Network
? null // For EIP 1559 we can get gas prices via "useGasFeeEstimates".
: dispatch(fetchAndSetSwapsGasPriceInfo());
const [[fetchedQuotes, selectedAggId]] = await Promise.all([
fetchAndSetQuotesPromise,
@ -616,6 +627,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
const state = getState();
const chainId = getCurrentChainId(state);
const hardwareWalletUsed = isHardwareWallet(state);
const EIP1559Network = isEIP1559Network(state);
let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
@ -637,6 +649,8 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
}
const customSwapsGas = getCustomSwapsGas(state);
const customMaxFeePerGas = getCustomMaxFeePerGas(state);
const customMaxPriorityFeePerGas = getCustomMaxPriorityFeePerGas(state);
const fetchParams = getFetchParams(state);
const { metaData, value: swapTokenValue, slippage } = fetchParams;
const { sourceTokenInfo = {}, destinationTokenInfo = {} } = metaData;
@ -649,6 +663,26 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
const { fast: fastGasEstimate } = getSwapGasPriceEstimateData(state);
let maxFeePerGas;
let maxPriorityFeePerGas;
let baseAndPriorityFeePerGas;
if (EIP1559Network) {
const {
high: { suggestedMaxFeePerGas, suggestedMaxPriorityFeePerGas },
estimatedBaseFee = '0',
} = getGasFeeEstimates(state);
maxFeePerGas =
customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas);
maxPriorityFeePerGas =
customMaxPriorityFeePerGas ||
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas);
baseAndPriorityFeePerGas = addHexes(
decGWEIToHexWEI(estimatedBaseFee),
maxPriorityFeePerGas,
);
}
const usedQuote = getUsedQuote(state);
const usedTradeTxParams = usedQuote.trade;
@ -668,7 +702,13 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
const usedGasPrice = getUsedSwapsGasPrice(state);
usedTradeTxParams.gas = maxGasLimit;
usedTradeTxParams.gasPrice = usedGasPrice;
if (EIP1559Network) {
usedTradeTxParams.maxFeePerGas = maxFeePerGas;
usedTradeTxParams.maxPriorityFeePerGas = maxPriorityFeePerGas;
delete usedTradeTxParams.gasPrice;
} else {
usedTradeTxParams.gasPrice = usedGasPrice;
}
const usdConversionRate = getUSDConversionRate(state);
const destinationValue = calcTokenAmount(
@ -682,7 +722,10 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
.plus(usedQuote.approvalNeeded?.gas || '0x0', 16)
.toString(16);
const gasEstimateTotalInUSD = getValueFromWeiHex({
value: calcGasTotal(totalGasLimitEstimate, usedGasPrice),
value: calcGasTotal(
totalGasLimitEstimate,
EIP1559Network ? baseAndPriorityFeePerGas : usedGasPrice,
),
toCurrency: 'usd',
conversionRate: usdConversionRate,
numberOfDecimals: 6,
@ -714,6 +757,11 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: getHardwareWalletType(state),
};
if (EIP1559Network) {
swapMetaData.max_fee_per_gas = maxFeePerGas;
swapMetaData.max_priority_fee_per_gas = maxPriorityFeePerGas;
swapMetaData.base_and_priority_fee_per_gas = baseAndPriorityFeePerGas;
}
metaMetricsEvent({
event: 'Swap Started',
@ -744,6 +792,11 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
}
if (approveTxParams) {
if (EIP1559Network) {
approveTxParams.maxFeePerGas = maxFeePerGas;
approveTxParams.maxPriorityFeePerGas = maxPriorityFeePerGas;
delete approveTxParams.gasPrice;
}
const approveTxMeta = await dispatch(
addUnapprovedTransaction(
{ ...approveTxParams, amount: '0x0' },

View File

@ -1,6 +1,6 @@
import nock from 'nock';
import { MOCKS } from '../../../test/jest';
import { MOCKS, createSwapsMockStore } from '../../../test/jest';
import { setSwapsLiveness } from '../../store/actions';
import { setStorageItem } from '../../helpers/utils/storage-helpers';
import * as swaps from './swaps';
@ -164,4 +164,82 @@ describe('Ducks - Swaps', () => {
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
});
});
describe('getCustomSwapsGas', () => {
it('returns "customMaxGas"', () => {
const state = createSwapsMockStore();
const customMaxGas = '29000';
state.metamask.swapsState.customMaxGas = customMaxGas;
expect(swaps.getCustomSwapsGas(state)).toBe(customMaxGas);
});
});
describe('getCustomMaxFeePerGas', () => {
it('returns "customMaxFeePerGas"', () => {
const state = createSwapsMockStore();
const customMaxFeePerGas = '20';
state.metamask.swapsState.customMaxFeePerGas = customMaxFeePerGas;
expect(swaps.getCustomMaxFeePerGas(state)).toBe(customMaxFeePerGas);
});
});
describe('getCustomMaxPriorityFeePerGas', () => {
it('returns "customMaxPriorityFeePerGas"', () => {
const state = createSwapsMockStore();
const customMaxPriorityFeePerGas = '3';
state.metamask.swapsState.customMaxPriorityFeePerGas = customMaxPriorityFeePerGas;
expect(swaps.getCustomMaxPriorityFeePerGas(state)).toBe(
customMaxPriorityFeePerGas,
);
});
});
describe('getSwapsFeatureIsLive', () => {
it('returns true for "swapsFeatureIsLive"', () => {
const state = createSwapsMockStore();
const swapsFeatureIsLive = true;
state.metamask.swapsState.swapsFeatureIsLive = swapsFeatureIsLive;
expect(swaps.getSwapsFeatureIsLive(state)).toBe(swapsFeatureIsLive);
});
it('returns false for "swapsFeatureIsLive"', () => {
const state = createSwapsMockStore();
const swapsFeatureIsLive = false;
state.metamask.swapsState.swapsFeatureIsLive = swapsFeatureIsLive;
expect(swaps.getSwapsFeatureIsLive(state)).toBe(swapsFeatureIsLive);
});
});
describe('getUseNewSwapsApi', () => {
it('returns true for "useNewSwapsApi"', () => {
const state = createSwapsMockStore();
const useNewSwapsApi = true;
state.metamask.swapsState.useNewSwapsApi = useNewSwapsApi;
expect(swaps.getUseNewSwapsApi(state)).toBe(useNewSwapsApi);
});
it('returns false for "useNewSwapsApi"', () => {
const state = createSwapsMockStore();
const useNewSwapsApi = false;
state.metamask.swapsState.useNewSwapsApi = useNewSwapsApi;
expect(swaps.getUseNewSwapsApi(state)).toBe(useNewSwapsApi);
});
});
describe('getUsedQuote', () => {
it('returns selected quote', () => {
const state = createSwapsMockStore();
expect(swaps.getUsedQuote(state)).toMatchObject(
state.metamask.swapsState.quotes.TEST_AGG_2,
);
});
it('returns best quote', () => {
const state = createSwapsMockStore();
state.metamask.swapsState.selectedAggId = null;
expect(swaps.getUsedQuote(state)).toMatchObject(
state.metamask.swapsState.quotes.TEST_AGG_BEST,
);
});
});
});

View File

@ -7,10 +7,10 @@ export const GAS_FORM_ERRORS = {
MAX_FEE_HIGH_WARNING: 'editGasMaxFeeHigh',
};
export function getGasFormErrorText(type, t) {
export function getGasFormErrorText(type, t, { minimumGasLimit } = {}) {
switch (type) {
case GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS:
return t('editGasLimitOutOfBounds');
return t('editGasLimitOutOfBounds', [minimumGasLimit]);
case GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW:
return t('editGasMaxPriorityFeeLow');
case GAS_FORM_ERRORS.MAX_FEE_TOO_LOW:

View File

@ -3,8 +3,15 @@ import { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { findKey } from 'lodash';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import { multiplyCurrencies } from '../../shared/modules/conversion.utils';
import {
GAS_ESTIMATE_TYPES,
EDIT_GAS_MODES,
GAS_LIMITS,
} from '../../shared/constants/gas';
import {
multiplyCurrencies,
conversionLessThan,
} from '../../shared/modules/conversion.utils';
import {
getMaximumGasTotalInHexWei,
getMinimumGasTotalInHexWei,
@ -152,7 +159,12 @@ function getMatchingEstimateFromGasFees(
* './useGasFeeEstimates'
* ).GasEstimates} - gas fee input state and the GasFeeEstimates object
*/
export function useGasFeeInputs(defaultEstimateToUse = 'medium', transaction) {
export function useGasFeeInputs(
defaultEstimateToUse = 'medium',
transaction,
minimumGasLimit,
editGasMode,
) {
// We need to know whether to show fiat conversions or not, so that we can
// default our fiat values to empty strings if showing fiat is not wanted or
// possible.
@ -257,9 +269,11 @@ export function useGasFeeInputs(defaultEstimateToUse = 'medium', transaction) {
// conditionally set to the appropriate fields to compute the minimum
// and maximum cost of a transaction given the current estimates or selected
// gas fees.
const gasSettings = {
gasLimit: decimalToHex(gasLimit),
};
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
gasSettings.maxFeePerGas = decGWEIToHexWEI(maxFeePerGasToUse);
gasSettings.maxPriorityFeePerGas = decGWEIToHexWEI(
@ -276,8 +290,18 @@ export function useGasFeeInputs(defaultEstimateToUse = 'medium', transaction) {
// The maximum amount this transaction will cost
const maximumCostInHexWei = getMaximumGasTotalInHexWei(gasSettings);
// If in swaps, we want to calculate the minimum gas fee differently than the max
const minGasSettings = {};
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
minGasSettings.gasLimit = decimalToHex(minimumGasLimit);
}
// The minimum amount this transaction will cost's
const minimumCostInHexWei = getMinimumGasTotalInHexWei(gasSettings);
const minimumCostInHexWei = getMinimumGasTotalInHexWei({
...gasSettings,
...minGasSettings,
});
// We need to display the estimated fiat currency impact of the
// maxPriorityFeePerGas field to the user. This hook calculates that amount.
@ -331,7 +355,12 @@ export function useGasFeeInputs(defaultEstimateToUse = 'medium', transaction) {
const gasErrors = {};
const gasWarnings = {};
if (gasLimit < 21000 || gasLimit > 7920027) {
const gasLimitTooLow = conversionLessThan(
{ value: gasLimit, fromNumericBase: 'dec' },
{ value: minimumGasLimit || GAS_LIMITS.SIMPLE, fromNumericBase: 'hex' },
);
if (gasLimitTooLow) {
gasErrors.gasLimit = GAS_FORM_ERRORS.GAS_LIMIT_OUT_OF_BOUNDS;
}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwapStepIcon renders the component 1`] = `
exports[`SwapStepIcon renders the component with step 1 by default 1`] = `
<div>
<svg
fill="none"
@ -23,3 +23,27 @@ exports[`SwapStepIcon renders the component 1`] = `
</svg>
</div>
`;
exports[`SwapStepIcon renders the component with step 2 1`] = `
<div>
<svg
fill="none"
height="14"
viewBox="0 0 14 14"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="7"
cy="7"
r="6.25"
stroke="#037DD6"
stroke-width="1.5"
/>
<path
d="M8.92 9.776H5V9.368C5 9.048 5.056 8.77067 5.168 8.536C5.28 8.296 5.42133 8.08533 5.592 7.904C5.768 7.71733 5.96267 7.54933 6.176 7.4C6.39467 7.25067 6.608 7.10133 6.816 6.952C6.928 6.872 7.03467 6.78933 7.136 6.704C7.24267 6.61867 7.33333 6.53067 7.408 6.44C7.488 6.34933 7.552 6.256 7.6 6.16C7.648 6.064 7.672 5.96533 7.672 5.864C7.672 5.67733 7.616 5.52 7.504 5.392C7.39733 5.25867 7.22933 5.192 7 5.192C6.88267 5.192 6.776 5.21333 6.68 5.256C6.584 5.29333 6.50133 5.344 6.432 5.408C6.368 5.472 6.31733 5.54667 6.28 5.632C6.248 5.71733 6.232 5.808 6.232 5.904H5.024C5.024 5.62667 5.07467 5.37067 5.176 5.136C5.27733 4.90133 5.41867 4.70133 5.6 4.536C5.78133 4.36533 5.99467 4.23467 6.24 4.144C6.48533 4.048 6.752 4 7.04 4C7.28 4 7.50933 4.03733 7.728 4.112C7.952 4.18667 8.14933 4.29867 8.32 4.448C8.49067 4.59733 8.62667 4.784 8.728 5.008C8.82933 5.22667 8.88 5.48267 8.88 5.776C8.88 6.032 8.85067 6.25867 8.792 6.456C8.73333 6.648 8.65067 6.824 8.544 6.984C8.44267 7.13867 8.32 7.28 8.176 7.408C8.032 7.536 7.87733 7.66133 7.712 7.784C7.64267 7.832 7.55733 7.888 7.456 7.952C7.36 8.016 7.26133 8.08267 7.16 8.152C7.064 8.22133 6.97333 8.29333 6.888 8.368C6.80267 8.44267 6.74133 8.51467 6.704 8.584H8.92V9.776Z"
fill="#037DD6"
/>
</svg>
</div>
`;

View File

@ -4,8 +4,13 @@ import { renderWithProvider } from '../../../../test/jest';
import SwapStepIcon from './swap-step-icon';
describe('SwapStepIcon', () => {
it('renders the component', () => {
it('renders the component with step 1 by default', () => {
const { container } = renderWithProvider(<SwapStepIcon />);
expect(container).toMatchSnapshot();
});
it('renders the component with step 2', () => {
const { container } = renderWithProvider(<SwapStepIcon stepNumber={2} />);
expect(container).toMatchSnapshot();
});
});

View File

@ -11,7 +11,7 @@ const createProps = (customProps = {}) => {
return {
swapComplete: false,
txHash: 'txHash',
tokensReceived: 'tokensReceived',
tokensReceived: 'tokens received:',
submittingSwap: true,
inputValue: 5,
maxSlippage: 3,
@ -34,4 +34,16 @@ describe('AwaitingSwap', () => {
).toMatchSnapshot();
expect(getByText('View in activity')).toBeInTheDocument();
});
it('renders the component with for completed swap', () => {
const store = configureMockStore()(createSwapsMockStore());
const { getByText } = renderWithProvider(
<AwaitingSwap {...createProps({ swapComplete: true })} />,
store,
);
expect(getByText('Transaction complete')).toBeInTheDocument();
expect(getByText('tokens received: ETH')).toBeInTheDocument();
expect(getByText('View at etherscan.io')).toBeInTheDocument();
expect(getByText('Create a new swap')).toBeInTheDocument();
});
});

View File

@ -1,5 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FeeCard renders the component with EIP-1559 enabled 1`] = `
<div
class="fee-card__savings-and-quotes-header"
data-testid="fee-card__savings-and-quotes-header"
>
<div
class="fee-card__savings-and-quotes-row"
>
<p
class="fee-card__savings-text"
>
Using the best quote
</p>
<div
class="fee-card__quote-link-container"
>
<p
class="fee-card__quote-link-text"
>
6 quotes
</p>
<div
class="fee-card__caret-right"
>
<i
class="fa fa-angle-up"
/>
</div>
</div>
</div>
</div>
`;
exports[`FeeCard renders the component with EIP-1559 enabled 2`] = `
<div
class="fee-card__top-bordered-row"
>
<div
class="fee-card__row-label"
>
<div
class="fee-card__row-header-text"
>
Quote includes a 0.875% MetaMask fee
</div>
<div
class="info-tooltip"
>
<div
class="fee-card__info-tooltip-container"
>
<div
aria-describedby="tippy-tooltip-6"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="#b8b8b8"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`FeeCard renders the component with initial props 1`] = `
<div
class="fee-card__savings-and-quotes-header"

View File

@ -2,12 +2,26 @@ import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import InfoTooltip from '../../../components/ui/info-tooltip';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import {
MAINNET_CHAIN_ID,
BSC_CHAIN_ID,
LOCALHOST_CHAIN_ID,
POLYGON_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../../shared/constants/network';
import TransactionDetail from '../../../components/app/transaction-detail/transaction-detail.component';
import TransactionDetailItem from '../../../components/app/transaction-detail-item/transaction-detail-item.component';
import GasTiming from '../../../components/app/gas-timing/gas-timing.component';
import Typography from '../../../components/ui/typography';
import {
COLORS,
TYPOGRAPHY,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
const GAS_FEES_LEARN_MORE_URL =
'https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172';
export default function FeeCard({
primaryFee,
@ -23,6 +37,8 @@ export default function FeeCard({
onQuotesClick,
tokenConversionRate,
chainId,
EIP1559Network,
maxPriorityFeePerGasDecGWEI,
}) {
const t = useContext(I18nContext);
@ -43,11 +59,18 @@ export default function FeeCard({
return t('networkNamePolygon');
case LOCALHOST_CHAIN_ID:
return t('networkNameTestnet');
case RINKEBY_CHAIN_ID:
return t('networkNameRinkeby');
default:
throw new Error('This network is not supported for token swaps');
}
};
const gasFeesLearnMoreLinkClickedEvent = useNewMetricEvent({
category: 'Swaps',
event: 'Clicked "Gas Fees: Learn More" Link',
});
return (
<div className="fee-card">
<div
@ -72,74 +95,154 @@ export default function FeeCard({
</div>
</div>
<div className="fee-card__main">
<div
className="fee-card__row-header"
data-testid="fee-card__row-header"
>
<div>
<div className="fee-card__row-header-text--bold">
{t('swapEstimatedNetworkFee')}
</div>
<InfoTooltip
position="top"
contentText={
<>
<p className="fee-card__info-tooltip-paragraph">
{t('swapNetworkFeeSummary', [getTranslatedNetworkName()])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapEstimatedNetworkFeeSummary', [
<span className="fee-card__bold" key="fee-card-bold-1">
{t('swapEstimatedNetworkFee')}
</span>,
])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapMaxNetworkFeeInfo', [
<span className="fee-card__bold" key="fee-card-bold-2">
{t('swapMaxNetworkFees')}
</span>,
])}
</p>
</>
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</div>
<div>
<div className="fee-card__row-header-secondary--bold">
{primaryFee.fee}
</div>
{secondaryFee && (
<div className="fee-card__row-header-primary--bold">
{secondaryFee.fee}
{EIP1559Network && (
<TransactionDetail
rows={[
<TransactionDetailItem
key="gas-item"
detailTitle={
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
position="top"
contentText={
<>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesSummary', [
getTranslatedNetworkName(),
])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesDetails')}
</p>
<p className="fee-card__info-tooltip-paragraph">
<a
className="fee-card__link"
onClick={() => {
gasFeesLearnMoreLinkClickedEvent();
global.platform.openTab({
url: GAS_FEES_LEARN_MORE_URL,
});
}}
target="_blank"
rel="noopener noreferrer"
>
{t('swapGasFeesLearnMore')}
</a>
</p>
</>
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</>
}
detailText={primaryFee.fee}
detailTotal={secondaryFee.fee}
subTitle={
<GasTiming
maxPriorityFeePerGas={maxPriorityFeePerGasDecGWEI}
/>
}
subText={
secondaryFee?.maxFee !== undefined && (
<>
<Typography
tag="span"
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.UI4}
variant={TYPOGRAPHY.H7}
>
{t('maxFee')}
</Typography>
{`: ${secondaryFee.maxFee}`}
<span
className="fee-card__edit-link"
onClick={() => onFeeCardMaxRowClick()}
>
{t('edit')}
</span>
</>
)
}
/>,
]}
/>
)}
{!EIP1559Network && (
<div
className="fee-card__row-header"
data-testid="fee-card__row-header"
>
<div>
<div className="fee-card__row-header-text--bold">
{t('swapEstimatedNetworkFee')}
</div>
)}
</div>
</div>
<div
className="fee-card__row-header"
onClick={() => onFeeCardMaxRowClick()}
>
<div>
<div className="fee-card__row-header-text">
{t('swapMaxNetworkFees')}
<InfoTooltip
position="top"
contentText={
<>
<p className="fee-card__info-tooltip-paragraph">
{t('swapNetworkFeeSummary', [getTranslatedNetworkName()])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapEstimatedNetworkFeeSummary', [
<span className="fee-card__bold" key="fee-card-bold-1">
{t('swapEstimatedNetworkFee')}
</span>,
])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapMaxNetworkFeeInfo', [
<span className="fee-card__bold" key="fee-card-bold-2">
{t('swapMaxNetworkFees')}
</span>,
])}
</p>
</>
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</div>
<div className="fee-card__link">{t('edit')}</div>
</div>
<div>
<div className="fee-card__row-header-secondary">
{primaryFee.maxFee}
</div>
{secondaryFee?.maxFee !== undefined && (
<div className="fee-card__row-header-primary">
{secondaryFee.maxFee}
<div>
<div className="fee-card__row-header-secondary--bold">
{primaryFee.fee}
</div>
)}
{secondaryFee && (
<div className="fee-card__row-header-primary--bold">
{secondaryFee.fee}
</div>
)}
</div>
</div>
</div>
)}
{!EIP1559Network && (
<div
className="fee-card__row-header"
onClick={() => onFeeCardMaxRowClick()}
>
<div>
<div className="fee-card__row-header-text">
{t('swapMaxNetworkFees')}
</div>
<div className="fee-card__link">{t('edit')}</div>
</div>
<div>
<div className="fee-card__row-header-secondary">
{primaryFee.maxFee}
</div>
{secondaryFee?.maxFee !== undefined && (
<div className="fee-card__row-header-primary">
{secondaryFee.maxFee}
</div>
)}
</div>
</div>
)}
{!hideTokenApprovalRow && (
<div className="fee-card__row-header">
<div className="fee-card__row-label">
@ -199,4 +302,6 @@ FeeCard.propTypes = {
numberOfQuotes: PropTypes.number.isRequired,
tokenConversionRate: PropTypes.number,
chainId: PropTypes.string.isRequired,
EIP1559Network: PropTypes.bool.isRequired,
maxPriorityFeePerGasDecGWEI: PropTypes.string,
};

View File

@ -1,9 +1,30 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../test/jest';
import {
renderWithProvider,
createSwapsMockStore,
MOCKS,
} from '../../../../test/jest';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
import FeeCard from '.';
const middleware = [thunk];
jest.mock('../../../hooks/useGasFeeEstimates', () => {
return {
useGasFeeEstimates: () => {
return {
gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(),
gasEstimateType: 'fee-market',
estimatedGasFeeTimeBounds: undefined,
isGasEstimatesLoading: false,
};
},
};
});
const createProps = (customProps = {}) => {
return {
primaryFee: {
@ -32,6 +53,7 @@ const createProps = (customProps = {}) => {
onQuotesClick: jest.fn(),
tokenConversionRate: 0.015,
chainId: MAINNET_CHAIN_ID,
EIP1559Network: false,
...customProps,
};
};
@ -58,4 +80,29 @@ describe('FeeCard', () => {
document.querySelector('.fee-card__top-bordered-row'),
).toMatchSnapshot();
});
it('renders the component with EIP-1559 enabled', () => {
const store = configureMockStore(middleware)(createSwapsMockStore());
const props = createProps({
EIP1559Network: true,
maxPriorityFeePerGasDecGWEI: '3',
});
const { getByText } = renderWithProvider(<FeeCard {...props} />, store);
expect(getByText('Using the best quote')).toBeInTheDocument();
expect(getByText('6 quotes')).toBeInTheDocument();
expect(getByText('Estimated gas fee')).toBeInTheDocument();
expect(getByText('Maybe in 5 minutes')).toBeInTheDocument();
expect(getByText(props.primaryFee.fee)).toBeInTheDocument();
expect(getByText(props.secondaryFee.fee)).toBeInTheDocument();
expect(getByText(`: ${props.secondaryFee.maxFee}`)).toBeInTheDocument();
expect(
getByText('Quote includes a 0.875% MetaMask fee'),
).toBeInTheDocument();
expect(
document.querySelector('.fee-card__savings-and-quotes-header'),
).toMatchSnapshot();
expect(
document.querySelector('.fee-card__top-bordered-row'),
).toMatchSnapshot();
});
});

View File

@ -21,8 +21,8 @@
border-top-right-radius: 8px;
border-top-left-radius: 8px;
border-bottom: 0;
padding-left: 8px;
padding-right: 8px;
padding-left: 16px;
padding-right: 16px;
}
&__savings-text {
@ -141,11 +141,18 @@
margin-right: 4px;
}
&__link {
&__link,
&__link:hover {
color: $Blue-500;
cursor: pointer;
}
&__edit-link {
color: $Blue-500;
cursor: pointer;
padding-left: 6px;
}
&__total-box {
border-top: 1px solid $Grey-100;
padding: 12px 16px 16px 16px;

View File

@ -36,6 +36,7 @@ import {
getUseNewSwapsApi,
getFromToken,
} from '../../ducks/swaps/swaps';
import { isEIP1559Network } from '../../ducks/metamask/metamask';
import {
AWAITING_SIGNATURES_ROUTE,
AWAITING_SWAP_ROUTE,
@ -63,7 +64,7 @@ import {
} from '../../store/actions';
import { currentNetworkTxListSelector } from '../../selectors';
import { useNewMetricEvent } from '../../hooks/useMetricEvent';
import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates';
import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import {
@ -111,8 +112,15 @@ export default function Swap() {
const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain);
const useNewSwapsApi = useSelector(getUseNewSwapsApi);
const EIP1559Network = useSelector(isEIP1559Network);
const fromToken = useSelector(getFromToken);
if (EIP1559Network) {
// This will pre-load gas fees before going to the View Quote page.
// eslint-disable-next-line react-hooks/rules-of-hooks
useGasFeeEstimates();
}
const {
balance: ethBalance,
address: selectedAccountAddress,
@ -187,12 +195,14 @@ export default function Swap() {
dispatch(setAggregatorMetadata(newAggregatorMetadata));
},
);
dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
if (!EIP1559Network) {
dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
}
return () => {
dispatch(prepareToLeaveSwaps());
};
}
}, [dispatch, chainId, isFeatureFlagLoaded, useNewSwapsApi]);
}, [dispatch, chainId, isFeatureFlagLoaded, useNewSwapsApi, EIP1559Network]);
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);

View File

@ -21,6 +21,8 @@ setBackgroundConnection({
setSwapsLiveness: jest.fn(() => true),
setSwapsTokens: jest.fn(),
setSwapsTxGasPrice: jest.fn(),
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
});
describe('Swap', () => {

View File

@ -59,10 +59,15 @@ const mapStateToProps = (state) => {
const customGasTotal = calcGasTotal(customGasLimit, customGasPrice);
const swapsGasPriceEstimates = getSwapGasPriceEstimateData(state);
const gasEstimates = getSwapGasPriceEstimateData(state);
const gasEstimatesInNewFormat = {
low: gasEstimates.safeLow,
medium: gasEstimates.average,
high: gasEstimates.fast,
};
const { averageEstimateData, fastEstimateData } = getRenderableGasButtonData(
swapsGasPriceEstimates,
gasEstimatesInNewFormat,
customGasLimit,
true,
conversionRate,

View File

@ -9,7 +9,9 @@ import {
ETHEREUM,
POLYGON,
BSC,
RINKEBY,
} from '../../../shared/constants/swaps';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
@ -21,6 +23,7 @@ import {
BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
LOCALHOST_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time';
import {
@ -656,6 +659,8 @@ export function getSwapsTokensReceivedFromTxMeta(
chainId,
) {
const txReceipt = txMeta?.txReceipt;
const EIP1559Network =
txMeta?.txReceipt?.type === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET;
if (isSwapsDefaultTokenSymbol(tokenSymbol, chainId)) {
if (
!txReceipt ||
@ -670,11 +675,16 @@ export function getSwapsTokensReceivedFromTxMeta(
if (approvalTxMeta && approvalTxMeta.txReceipt) {
approvalTxGasCost = calcGasTotal(
approvalTxMeta.txReceipt.gasUsed,
approvalTxMeta.txParams.gasPrice,
EIP1559Network
? approvalTxMeta.txReceipt.effectiveGasPrice // Base fee + priority fee.
: approvalTxMeta.txParams.gasPrice,
);
}
const gasCost = calcGasTotal(txReceipt.gasUsed, txMeta.txParams.gasPrice);
const gasCost = calcGasTotal(
txReceipt.gasUsed,
EIP1559Network ? txReceipt.effectiveGasPrice : txMeta.txParams.gasPrice,
);
const totalGasCost = new BigNumber(gasCost, 16)
.plus(approvalTxGasCost, 16)
.toString(16);
@ -786,6 +796,8 @@ export const getNetworkNameByChainId = (chainId) => {
return BSC;
case POLYGON_CHAIN_ID:
return POLYGON;
case RINKEBY_CHAIN_ID:
return RINKEBY;
default:
return '';
}
@ -799,8 +811,8 @@ export const getNetworkNameByChainId = (chainId) => {
*/
export const getSwapsLivenessForNetwork = (swapsFeatureFlags = {}, chainId) => {
const networkName = getNetworkNameByChainId(chainId);
// Use old APIs for testnet.
if (chainId === LOCALHOST_CHAIN_ID) {
// Use old APIs for testnet and Rinkeby.
if ([LOCALHOST_CHAIN_ID, RINKEBY_CHAIN_ID].includes(chainId)) {
return {
swapsFeatureIsLive: true,
useNewSwapsApi: false,

View File

@ -8,6 +8,7 @@ import {
POLYGON_CHAIN_ID,
LOCALHOST_CHAIN_ID,
RINKEBY_CHAIN_ID,
KOVAN_CHAIN_ID,
} from '../../../shared/constants/network';
import {
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
@ -15,6 +16,7 @@ import {
ETHEREUM,
POLYGON,
BSC,
RINKEBY,
} from '../../../shared/constants/swaps';
import {
TOKENS,
@ -394,8 +396,12 @@ describe('Swaps Util', () => {
expect(getNetworkNameByChainId(POLYGON_CHAIN_ID)).toBe(POLYGON);
});
it('returns "rinkeby" for Rinkeby chain ID', () => {
expect(getNetworkNameByChainId(RINKEBY_CHAIN_ID)).toBe(RINKEBY);
});
it('returns an empty string for an unsupported network', () => {
expect(getNetworkNameByChainId(RINKEBY_CHAIN_ID)).toBe('');
expect(getNetworkNameByChainId(KOVAN_CHAIN_ID)).toBe('');
});
});
@ -413,6 +419,19 @@ describe('Swaps Util', () => {
).toMatchObject(expectedSwapsLiveness);
});
it('returns info that Swaps are enabled and cannot use API v2 for Rinkeby chain ID', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
expect(
getSwapsLivenessForNetwork(
MOCKS.createFeatureFlagsResponse(),
RINKEBY_CHAIN_ID,
),
).toMatchObject(expectedSwapsLiveness);
});
it('returns info that Swaps are disabled and cannot use API v2 if network name is not found', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
@ -421,7 +440,7 @@ describe('Swaps Util', () => {
expect(
getSwapsLivenessForNetwork(
MOCKS.createFeatureFlagsResponse(),
RINKEBY_CHAIN_ID,
KOVAN_CHAIN_ID,
),
).toMatchObject(expectedSwapsLiveness);
});

View File

@ -1,5 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewQuote renders the component with EIP-1559 enabled 1`] = `
<div
class="main-quote-summary__source-row"
data-testid="main-quote-summary__source-row"
>
<span
class="main-quote-summary__source-row-value"
title="10"
>
10
</span>
<img
alt=""
class="url-icon main-quote-summary__icon"
src="https://foo.bar/logo.png"
/>
<span
class="main-quote-summary__source-row-symbol"
title="DAI"
>
DAI
</span>
</div>
`;
exports[`ViewQuote renders the component with EIP-1559 enabled 2`] = `
<div
class="main-quote-summary__exchange-rate-container"
data-testid="main-quote-summary__exchange-rate-container"
>
<div
class="exchange-rate-display main-quote-summary__exchange-rate-display"
>
<span>
1
</span>
<span
class=""
>
DAI
</span>
<span>
=
</span>
<span>
2.2
</span>
<span
class=""
>
USDC
</span>
<div
class="exchange-rate-display__switch-arrows"
>
<svg
fill="none"
height="13"
viewBox="0 0 13 13"
width="13"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.15294 4.38514H9.99223L8.50853 5.86884C8.30421 6.07297 8.30421 6.40418 8.50853 6.60869C8.61069 6.71085 8.74443 6.76203 8.87836 6.76203C9.01229 6.76203 9.14603 6.71085 9.24819 6.60869L11.6249 4.23219C11.649 4.20803 11.6707 4.1814 11.6899 4.15305C11.6947 4.14563 11.6981 4.13726 11.7025 4.12965C11.7154 4.10815 11.7282 4.08646 11.7381 4.06325C11.7426 4.05222 11.7447 4.04043 11.7487 4.0292C11.7558 4.00827 11.7636 3.98754 11.7681 3.96547C11.775 3.93161 11.7786 3.89717 11.7786 3.86198C11.7786 3.82678 11.775 3.79235 11.7681 3.75849C11.7638 3.73642 11.756 3.71568 11.7487 3.69476C11.7447 3.68353 11.7428 3.67174 11.7381 3.6607C11.7282 3.63749 11.7156 3.616 11.7025 3.59431C11.6981 3.5867 11.6947 3.57833 11.6899 3.57091C11.6707 3.54256 11.649 3.51593 11.6249 3.49177L9.24876 1.11564C9.04444 0.911322 8.71342 0.911322 8.50891 1.11564C8.30459 1.31977 8.30459 1.65098 8.50891 1.85549L9.99223 3.339H4.15294C2.22978 3.339 0.665039 4.90374 0.665039 6.8269C0.665039 7.11588 0.899227 7.35007 1.1882 7.35007C1.47718 7.35007 1.71137 7.11588 1.71137 6.8269C1.71137 5.48037 2.80659 4.38514 4.15294 4.38514ZM12.2066 6.57445C11.9177 6.57445 11.6835 6.80864 11.6835 7.09762C11.6835 8.44396 10.5883 9.53919 9.24191 9.53919H3.40262L4.88632 8.05549C5.09064 7.85136 5.09064 7.52014 4.88632 7.31563C4.682 7.11112 4.35098 7.11131 4.14647 7.31563L1.76977 9.69233C1.74561 9.71649 1.72393 9.74312 1.70471 9.77147C1.70015 9.7787 1.69691 9.78669 1.69273 9.79392C1.6796 9.81561 1.66647 9.83748 1.65677 9.86126C1.6524 9.87211 1.6503 9.88371 1.64631 9.89475C1.63927 9.91586 1.63128 9.93679 1.62671 9.95905C1.61986 9.99291 1.61625 10.0273 1.61625 10.0625C1.61625 10.0977 1.61986 10.1322 1.62671 10.166C1.63109 10.1883 1.63908 10.2092 1.64631 10.2303C1.6503 10.2414 1.65221 10.253 1.65677 10.2638C1.66666 10.2874 1.6796 10.3093 1.69273 10.3312C1.69691 10.3384 1.70015 10.3464 1.70471 10.3536C1.72393 10.382 1.74561 10.4086 1.76977 10.4328L4.14609 12.8091C4.24825 12.9112 4.38199 12.9624 4.51592 12.9624C4.64985 12.9624 4.78359 12.9112 4.88575 12.8091C5.09007 12.6049 5.09007 12.2737 4.88575 12.0692L3.40243 10.5857H9.24172C11.1649 10.5857 12.7296 9.02097 12.7296 7.09781C12.7298 6.80864 12.4956 6.57445 12.2066 6.57445Z"
fill="#037DD6"
/>
</svg>
</div>
</div>
</div>
`;
exports[`ViewQuote renders the component with EIP-1559 enabled 3`] = `
<div
class="fee-card__savings-and-quotes-header"
data-testid="fee-card__savings-and-quotes-header"
>
<div
class="fee-card__savings-and-quotes-row"
>
<div
class="fee-card__quote-link-container"
>
<p
class="fee-card__quote-link-text"
>
3 quotes
</p>
<div
class="fee-card__caret-right"
>
<i
class="fa fa-angle-up"
/>
</div>
</div>
</div>
</div>
`;
exports[`ViewQuote renders the component with initial props 1`] = `
<div
class="main-quote-summary__source-row"

View File

@ -10,8 +10,10 @@ import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
import { useEqualityCheck } from '../../../hooks/useEqualityCheck';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { usePrevious } from '../../../hooks/usePrevious';
import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import FeeCard from '../fee-card';
import EditGasPopover from '../../../components/app/edit-gas-popover/edit-gas-popover.component';
import {
FALLBACK_GAS_MULTIPLIER,
getQuotes,
@ -21,7 +23,9 @@ import {
setBalanceError,
getQuotesLastFetched,
getBalanceError,
getCustomSwapsGas,
getCustomSwapsGas, // Gas limit.
getCustomMaxFeePerGas,
getCustomMaxPriorityFeePerGas,
getDestinationTokenInfo,
getUsedSwapsGasPrice,
getTopQuote,
@ -41,7 +45,11 @@ import {
isHardwareWallet,
getHardwareWalletType,
} from '../../../selectors';
import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask';
import {
getNativeCurrency,
getTokens,
isEIP1559Network,
} from '../../../ducks/metamask/metamask';
import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util';
@ -68,6 +76,9 @@ import {
decimalToHex,
hexToDecimal,
getValueFromWeiHex,
decGWEIToHexWEI,
hexWEIToDecGWEI,
addHexes,
} from '../../../helpers/utils/conversions.util';
import MainQuoteSummary from '../main-quote-summary';
import { calcGasTotal } from '../../send/send.utils';
@ -79,6 +90,7 @@ import {
} from '../swaps.util';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { QUOTES_EXPIRED_ERROR } from '../../../../shared/constants/swaps';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import CountdownTimer from '../countdown-timer';
import SwapsFooter from '../swaps-footer';
import ViewQuotePriceDifference from './view-quote-price-difference';
@ -94,6 +106,7 @@ export default function ViewQuote() {
const [selectQuotePopoverShown, setSelectQuotePopoverShown] = useState(false);
const [warningHidden, setWarningHidden] = useState(false);
const [originalApproveAmount, setOriginalApproveAmount] = useState(null);
const [showEditGasPopover, setShowEditGasPopover] = useState(false);
const [
acknowledgedPriceDifference,
@ -116,12 +129,15 @@ export default function ViewQuote() {
// Select necessary data
const gasPrice = useSelector(getUsedSwapsGasPrice);
const customMaxGas = useSelector(getCustomSwapsGas);
const customMaxFeePerGas = useSelector(getCustomMaxFeePerGas);
const customMaxPriorityFeePerGas = useSelector(getCustomMaxPriorityFeePerGas);
const tokenConversionRates = useSelector(getTokenExchangeRates);
const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates);
const { balance: ethBalance } = useSelector(getSelectedAccount);
const conversionRate = useSelector(conversionRateSelector);
const currentCurrency = useSelector(getCurrentCurrency);
const swapsTokens = useSelector(getTokens);
const EIP1559Network = useSelector(isEIP1559Network);
const balanceError = useSelector(getBalanceError);
const fetchParams = useSelector(getFetchParams);
const approveTxParams = useSelector(getApproveTxParams);
@ -134,6 +150,13 @@ export default function ViewQuote() {
const chainId = useSelector(getCurrentChainId);
const nativeCurrencySymbol = useSelector(getNativeCurrency);
let gasFeeInputs;
if (EIP1559Network) {
// For Swaps we want to get 'high' estimations by default.
// eslint-disable-next-line react-hooks/rules-of-hooks
gasFeeInputs = useGasFeeInputs('high');
}
const { isBestQuote } = usedQuote;
const fetchParamsSourceToken = fetchParams?.sourceToken;
@ -154,7 +177,30 @@ export default function ViewQuote() {
: `0x${decimalToHex(usedQuote?.maxGas || 0)}`;
const maxGasLimit = customMaxGas || nonCustomMaxGasLimit;
const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice);
let maxFeePerGas;
let maxPriorityFeePerGas;
let baseAndPriorityFeePerGas;
if (EIP1559Network) {
const {
maxFeePerGas: suggestedMaxFeePerGas,
maxPriorityFeePerGas: suggestedMaxPriorityFeePerGas,
gasFeeEstimates: { estimatedBaseFee = '0' },
} = gasFeeInputs;
maxFeePerGas = customMaxFeePerGas || decGWEIToHexWEI(suggestedMaxFeePerGas);
maxPriorityFeePerGas =
customMaxPriorityFeePerGas ||
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas);
baseAndPriorityFeePerGas = addHexes(
decGWEIToHexWEI(estimatedBaseFee),
maxPriorityFeePerGas,
);
}
const gasTotalInWeiHex = calcGasTotal(
maxGasLimit,
EIP1559Network ? maxFeePerGas : gasPrice,
);
const { tokensWithBalances } = useTokenTracker(swapsTokens, true);
const balanceToken =
@ -185,7 +231,7 @@ export default function ViewQuote() {
const renderablePopoverData = useMemo(() => {
return quotesToRenderableData(
quotes,
gasPrice,
EIP1559Network ? baseAndPriorityFeePerGas : gasPrice,
conversionRate,
currentCurrency,
approveGas,
@ -195,6 +241,8 @@ export default function ViewQuote() {
}, [
quotes,
gasPrice,
baseAndPriorityFeePerGas,
EIP1559Network,
conversionRate,
currentCurrency,
approveGas,
@ -221,7 +269,7 @@ export default function ViewQuote() {
const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({
tradeGas: usedGasLimit,
approveGas,
gasPrice,
gasPrice: EIP1559Network ? baseAndPriorityFeePerGas : gasPrice,
currentCurrency,
conversionRate,
tradeValue,
@ -238,7 +286,7 @@ export default function ViewQuote() {
} = getRenderableNetworkFeesForQuote({
tradeGas: maxGasLimit,
approveGas,
gasPrice,
gasPrice: EIP1559Network ? maxFeePerGas : gasPrice,
currentCurrency,
conversionRate,
tradeValue,
@ -455,7 +503,10 @@ export default function ViewQuote() {
};
const nonGasFeeIsPositive = new BigNumber(nonGasFee, 16).gt(0);
const approveGasTotal = calcGasTotal(approveGas || '0x0', gasPrice);
const approveGasTotal = calcGasTotal(
approveGas || '0x0',
EIP1559Network ? baseAndPriorityFeePerGas : gasPrice,
);
const extraNetworkFeeTotalInHexWEI = new BigNumber(nonGasFee, 16)
.plus(approveGasTotal, 16)
.toString(16);
@ -474,26 +525,29 @@ export default function ViewQuote() {
extraInfoRowLabel = t('aggregatorFeeCost');
}
const onFeeCardMaxRowClick = () =>
dispatch(
showModal({
name: 'CUSTOMIZE_METASWAP_GAS',
value: tradeValue,
customGasLimitMessage: approveGas
? t('extraApprovalGas', [hexToDecimal(approveGas)])
: '',
customTotalSupplement: approveGasTotal,
extraInfoRow: extraInfoRowLabel
? {
label: extraInfoRowLabel,
value: `${extraNetworkFeeTotalInEth} ${nativeCurrencySymbol}`,
}
: null,
initialGasPrice: gasPrice,
initialGasLimit: maxGasLimit,
minimumGasLimit: new BigNumber(nonCustomMaxGasLimit, 16).toNumber(),
}),
);
const onFeeCardMaxRowClick = () => {
EIP1559Network
? setShowEditGasPopover(true)
: dispatch(
showModal({
name: 'CUSTOMIZE_METASWAP_GAS',
value: tradeValue,
customGasLimitMessage: approveGas
? t('extraApprovalGas', [hexToDecimal(approveGas)])
: '',
customTotalSupplement: approveGasTotal,
extraInfoRow: extraInfoRowLabel
? {
label: extraInfoRowLabel,
value: `${extraNetworkFeeTotalInEth} ${nativeCurrencySymbol}`,
}
: null,
initialGasPrice: gasPrice,
initialGasLimit: maxGasLimit,
minimumGasLimit: new BigNumber(nonCustomMaxGasLimit, 16).toNumber(),
}),
);
};
const tokenApprovalTextComponent = (
<span key="swaps-view-quote-approve-symbol-1" className="view-quote__bold">
@ -590,6 +644,10 @@ export default function ViewQuote() {
const isShowingWarning =
showInsufficientWarning || shouldShowPriceDifferenceWarning;
const onCloseEditGasPopover = () => {
setShowEditGasPopover(false);
};
return (
<div className="view-quote">
<div
@ -607,6 +665,24 @@ export default function ViewQuote() {
onQuoteDetailsIsOpened={quoteDetailsOpened}
/>
)}
{showEditGasPopover && EIP1559Network && (
<EditGasPopover
transaction={{
txParams: {
maxFeePerGas,
maxPriorityFeePerGas,
gas: maxGasLimit,
},
}}
minimumGasLimit={usedGasLimit}
defaultEstimateToUse="high"
mode={EDIT_GAS_MODES.SWAPS}
confirmButtonText={t('submit')}
onClose={onCloseEditGasPopover}
/>
)}
<div
className={classnames('view-quote__warning-wrapper', {
'view-quote__warning-wrapper--thin': !isShowingWarning,
@ -676,6 +752,8 @@ export default function ViewQuote() {
: memoizedTokenConversionRates[destinationToken.address]
}
chainId={chainId}
EIP1559Network={EIP1559Network}
maxPriorityFeePerGasDecGWEI={hexWEIToDecGWEI(maxPriorityFeePerGas)}
/>
</div>
</div>
@ -697,8 +775,8 @@ export default function ViewQuote() {
balanceError ||
tokenBalanceUnavailable ||
disableSubmissionDueToPriceWarning ||
gasPrice === null ||
gasPrice === undefined
(EIP1559Network && baseAndPriorityFeePerGas === undefined) ||
(!EIP1559Network && (gasPrice === null || gasPrice === undefined))
}
className={isShowingWarning && 'view-quote__thin-swaps-footer'}
showTopBorder

View File

@ -6,6 +6,7 @@ import {
renderWithProvider,
createSwapsMockStore,
setBackgroundConnection,
MOCKS,
} from '../../../../test/jest';
import ViewQuote from '.';
@ -13,6 +14,18 @@ jest.mock('../../../components/ui/info-tooltip/info-tooltip-icon', () => () =>
'<InfoTooltipIcon />',
);
jest.mock('../../../hooks/useGasFeeInputs', () => {
return {
useGasFeeInputs: () => {
return {
maxFeePerGas: 16,
maxPriorityFeePerGas: 3,
gasFeeEstimates: MOCKS.createGasFeeEstimatesForFeeMarket(),
};
},
};
});
const middleware = [thunk];
const createProps = (customProps = {}) => {
return {
@ -31,6 +44,8 @@ setBackgroundConnection({
resetPostFetchState: jest.fn(),
safeRefetchQuotes: jest.fn(),
setSwapsErrorKey: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
updateTransaction: jest.fn(),
});
describe('ViewQuote', () => {
@ -53,4 +68,32 @@ describe('ViewQuote', () => {
expect(getByText('Back')).toBeInTheDocument();
expect(getByText('Swap')).toBeInTheDocument();
});
it('renders the component with EIP-1559 enabled', () => {
const state = createSwapsMockStore();
state.metamask.networkDetails = {
EIPS: {
1559: true,
},
};
const store = configureMockStore(middleware)(state);
const props = createProps();
const { getByText, getByTestId } = renderWithProvider(
<ViewQuote {...props} />,
store,
);
expect(getByText('New quotes in')).toBeInTheDocument();
expect(getByTestId('main-quote-summary__source-row')).toMatchSnapshot();
expect(
getByTestId('main-quote-summary__exchange-rate-container'),
).toMatchSnapshot();
expect(
getByTestId('fee-card__savings-and-quotes-header'),
).toMatchSnapshot();
expect(getByText('Estimated gas fee')).toBeInTheDocument();
expect(getByText('0.01044 ETH')).toBeInTheDocument();
expect(getByText('Max fee')).toBeInTheDocument();
expect(getByText('Back')).toBeInTheDocument();
expect(getByText('Swap')).toBeInTheDocument();
});
});

View File

@ -2199,6 +2199,23 @@ export function setSwapsTxGasLimit(gasLimit) {
};
}
export function updateCustomSwapsEIP1559GasParams({
gasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
}) {
return async (dispatch) => {
await Promise.all([
promisifiedBackground.setSwapsTxGasLimit(gasLimit),
promisifiedBackground.setSwapsTxMaxFeePerGas(maxFeePerGas),
promisifiedBackground.setSwapsTxMaxFeePriorityPerGas(
maxPriorityFeePerGas,
),
]);
await forceUpdateMetamaskState(dispatch);
};
}
export function customSwapsGasParamsUpdated(gasLimit, gasPrice) {
return async (dispatch) => {
await promisifiedBackground.setSwapsTxGasPrice(gasPrice);