Improvements for Smart Transactions / Swaps (#14022)

- Don’t call /estimateGas if a user doesn’t have enough funds
- Hardcode block explorer URLs for Swaps
- Track the "stx_prev_user_opt_in" param
- Add fee estimates tracking for regular txs and STX
- Track estimated_gas and estimated_vs_used_gasRatio for STX
- Only track the "Error Smart Transactions" event once
- Don't overwrite "maxGasLimit" for STX on the View Quote page for better "balance needed" estimations
- Update description for Transak
- Fix styles for the input field on the Build Quote page
- Refactor variables for STX error types and add translation for each STX error type
- Do additional logging for the "current_stx_enabled" param
- Add a close icon for an STX notification, update STX content
feature/default_network_editable
Daniel 3 years ago committed by GitHub
parent 386a760285
commit 4c16b583c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      app/_locales/en/messages.json
  2. 33
      ui/ducks/swaps/swaps.js
  3. 1
      ui/ducks/swaps/swaps.test.js
  4. 5
      ui/pages/swaps/awaiting-signatures/awaiting-signatures.js
  5. 5
      ui/pages/swaps/awaiting-swap/awaiting-swap.js
  6. 8
      ui/pages/swaps/build-quote/build-quote.js
  7. 7
      ui/pages/swaps/build-quote/index.scss
  8. 5
      ui/pages/swaps/dropdown-search-list/dropdown-search-list.js
  9. 43
      ui/pages/swaps/index.js
  10. 24
      ui/pages/swaps/index.scss
  11. 5
      ui/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js
  12. 7
      ui/pages/swaps/slippage-buttons/slippage-buttons.js
  13. 2
      ui/pages/swaps/slippage-buttons/slippage-buttons.test.js
  14. 8
      ui/pages/swaps/smart-transaction-status/smart-transaction-status.js
  15. 51
      ui/pages/swaps/swaps.util.js
  16. 158
      ui/pages/swaps/view-quote/view-quote.js
  17. 6
      ui/store/actions.js

@ -410,15 +410,15 @@
"description": "$1 represents the cypto symbol to be purchased" "description": "$1 represents the cypto symbol to be purchased"
}, },
"buyCryptoWithMoonPayDescription": { "buyCryptoWithMoonPayDescription": {
"message": "MoonPay supports popular payment methods, including Visa, Mastercard, Apple / Google / Samsung Pay, and bank transfers in 146+ countries. Tokens deposit into your MetaMask account." "message": "MoonPay supports popular payment methods, including Visa, Mastercard, Apple / Google / Samsung Pay, and bank transfers in 145+ countries. Tokens deposit into your MetaMask account."
}, },
"buyCryptoWithTransak": { "buyCryptoWithTransak": {
"message": "Buy $1 with Transak", "message": "Buy $1 with Transak",
"description": "$1 represents the cypto symbol to be purchased" "description": "$1 represents the cypto symbol to be purchased"
}, },
"buyCryptoWithTransakDescription": { "buyCryptoWithTransakDescription": {
"message": "Transak supports debit card and bank transfers (depending on location) in 59+ countries. $1 deposits into your MetaMask account.", "message": "Transak supports credit & debit cards, Apple Pay, MobiKwik, and bank transfers (depending on location) in 100+ countries. $1 deposits directly into your MetaMask account.",
"description": "$1 represents the cypto symbol to be purchased" "description": "$1 represents the crypto symbol to be purchased"
}, },
"buyEth": { "buyEth": {
"message": "Buy ETH" "message": "Buy ETH"
@ -2857,7 +2857,7 @@
"message": "Slow" "message": "Slow"
}, },
"smartTransaction": { "smartTransaction": {
"message": "Smart transaction" "message": "Smart Transaction"
}, },
"snapAccess": { "snapAccess": {
"message": "$1 snap has access to:", "message": "$1 snap has access to:",
@ -3047,6 +3047,12 @@
"stxDescription": { "stxDescription": {
"message": "MetaMask Swaps just got a whole lot smarter! Enabling Smart Transactions will allow MetaMask to programmatically optimize your Swap to help:" "message": "MetaMask Swaps just got a whole lot smarter! Enabling Smart Transactions will allow MetaMask to programmatically optimize your Swap to help:"
}, },
"stxErrorNotEnoughFunds": {
"message": "Not enough funds for a smart transaction."
},
"stxErrorUnavailable": {
"message": "Smart Transactions are temporarily unavailable."
},
"stxFailure": { "stxFailure": {
"message": "Swap failed" "message": "Swap failed"
}, },
@ -3054,8 +3060,11 @@
"message": "Sudden market changes can cause failures. If the problem persists, please reach out to $1.", "message": "Sudden market changes can cause failures. If the problem persists, please reach out to $1.",
"description": "This message is shown to a user if their swap fails. The $1 will be replaced by support.metamask.io" "description": "This message is shown to a user if their swap fails. The $1 will be replaced by support.metamask.io"
}, },
"stxFallbackToNormal": { "stxFallbackPendingTx": {
"message": "You can still swap using the normal method or wait for cheaper gas fees and less failures with smart transactions." "message": "Smart Transactions are temporarily unavailable because you have a pending transaction."
},
"stxFallbackUnavailable": {
"message": "You can still swap your tokens even while Smart Transactions are unavailable."
}, },
"stxPendingFinalizing": { "stxPendingFinalizing": {
"message": "Finalizing..." "message": "Finalizing..."
@ -3082,8 +3091,11 @@
"stxTryRegular": { "stxTryRegular": {
"message": "Try a regular swap." "message": "Try a regular swap."
}, },
"stxTryingToCancel": {
"message": "Trying to cancel your transaction..."
},
"stxUnavailable": { "stxUnavailable": {
"message": "Smart transactions temporarily unavailable" "message": "Smart Transactions are disabled"
}, },
"stxUnknown": { "stxUnknown": {
"message": "Status unknown" "message": "Status unknown"

@ -33,6 +33,7 @@ import {
fetchSmartTransactionFees, fetchSmartTransactionFees,
estimateSmartTransactionsGas, estimateSmartTransactionsGas,
cancelSmartTransaction, cancelSmartTransaction,
getTransactions,
} from '../../store/actions'; } from '../../store/actions';
import { import {
AWAITING_SIGNATURES_ROUTE, AWAITING_SIGNATURES_ROUTE,
@ -81,6 +82,7 @@ import {
} from '../../../shared/constants/swaps'; } from '../../../shared/constants/swaps';
import { import {
TRANSACTION_TYPES, TRANSACTION_TYPES,
TRANSACTION_STATUSES,
SMART_TRANSACTION_STATUSES, SMART_TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { getGasFeeEstimates } from '../metamask/metamask'; import { getGasFeeEstimates } from '../metamask/metamask';
@ -199,9 +201,9 @@ const slice = createSlice({
state.customGas.fallBackPrice = action.payload; state.customGas.fallBackPrice = action.payload;
}, },
setCurrentSmartTransactionsError: (state, action) => { setCurrentSmartTransactionsError: (state, action) => {
const errorType = stxErrorTypes.includes(action.payload) const errorType = Object.values(stxErrorTypes).includes(action.payload)
? action.payload ? action.payload
: stxErrorTypes[0]; : stxErrorTypes.UNAVAILABLE;
state.currentSmartTransactionsError = errorType; state.currentSmartTransactionsError = errorType;
}, },
dismissCurrentSmartTransactionsErrorMessage: (state) => { dismissCurrentSmartTransactionsErrorMessage: (state) => {
@ -554,12 +556,24 @@ export const fetchSwapsLivenessAndFeatureFlags = () => {
let swapsLivenessForNetwork = { let swapsLivenessForNetwork = {
swapsFeatureIsLive: false, swapsFeatureIsLive: false,
}; };
const chainId = getCurrentChainId(getState()); const state = getState();
const chainId = getCurrentChainId(state);
try { try {
const swapsFeatureFlags = await fetchSwapsFeatureFlags(); const swapsFeatureFlags = await fetchSwapsFeatureFlags();
await dispatch(setSwapsFeatureFlags(swapsFeatureFlags)); await dispatch(setSwapsFeatureFlags(swapsFeatureFlags));
if (ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS.includes(chainId)) { if (ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS.includes(chainId)) {
await dispatch(fetchSmartTransactionsLiveness()); await dispatch(fetchSmartTransactionsLiveness());
const pendingTransactions = await getTransactions({
searchCriteria: {
status: TRANSACTION_STATUSES.PENDING,
from: state.metamask?.selectedAddress,
},
});
if (pendingTransactions?.length > 0) {
dispatch(
setCurrentSmartTransactionsError(stxErrorTypes.REGULAR_TX_PENDING),
);
}
} }
swapsLivenessForNetwork = getSwapsLivenessForNetwork( swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags, swapsFeatureFlags,
@ -820,6 +834,7 @@ export const signAndSendSwapsSmartTransaction = ({
unsignedTransaction, unsignedTransaction,
metaMetricsEvent, metaMetricsEvent,
history, history,
additionalTrackingParams,
}) => { }) => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
dispatch(setSwapsSTXSubmitLoading(true)); dispatch(setSwapsSTXSubmitLoading(true));
@ -871,6 +886,7 @@ export const signAndSendSwapsSmartTransaction = ({
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
...additionalTrackingParams,
}; };
metaMetricsEvent({ metaMetricsEvent({
event: 'STX Swap Started', event: 'STX Swap Started',
@ -966,7 +982,11 @@ export const signAndSendSwapsSmartTransaction = ({
}; };
}; };
export const signAndSendTransactions = (history, metaMetricsEvent) => { export const signAndSendTransactions = (
history,
metaMetricsEvent,
additionalTrackingParams,
) => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
@ -1079,6 +1099,9 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
}); });
const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state); const smartTransactionsOptInStatus = getSmartTransactionsOptInStatus(state);
const smartTransactionsEnabled = getSmartTransactionsEnabled(state); const smartTransactionsEnabled = getSmartTransactionsEnabled(state);
const currentSmartTransactionsEnabled = getCurrentSmartTransactionsEnabled(
state,
);
const swapMetaData = { const swapMetaData = {
token_from: sourceTokenInfo.symbol, token_from: sourceTokenInfo.symbol,
token_from_amount: String(swapTokenValue), token_from_amount: String(swapTokenValue),
@ -1105,7 +1128,9 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: getHardwareWalletType(state), hardware_wallet_type: getHardwareWalletType(state),
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
...additionalTrackingParams,
}; };
if (networkAndAccountSupports1559) { if (networkAndAccountSupports1559) {
swapMetaData.max_fee_per_gas = maxFeePerGas; swapMetaData.max_fee_per_gas = maxFeePerGas;

@ -15,6 +15,7 @@ jest.mock('../../store/actions.js', () => ({
setSwapsLiveness: jest.fn(), setSwapsLiveness: jest.fn(),
setSwapsFeatureFlags: jest.fn(), setSwapsFeatureFlags: jest.fn(),
fetchSmartTransactionsLiveness: jest.fn(), fetchSmartTransactionsLiveness: jest.fn(),
getTransactions: jest.fn(),
})); }));
const providerState = { const providerState = {

@ -11,6 +11,7 @@ import {
prepareToLeaveSwaps, prepareToLeaveSwaps,
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getCurrentSmartTransactionsEnabled,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
import { import {
isHardwareWallet, isHardwareWallet,
@ -47,6 +48,9 @@ export default function AwaitingSignatures() {
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
); );
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const needsTwoConfirmations = Boolean(approveTxParams); const needsTwoConfirmations = Boolean(approveTxParams);
const awaitingSignaturesEvent = useNewMetricEvent({ const awaitingSignaturesEvent = useNewMetricEvent({
@ -62,6 +66,7 @@ export default function AwaitingSignatures() {
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}, },
category: 'swaps', category: 'swaps',

@ -31,6 +31,7 @@ import {
prepareToLeaveSwaps, prepareToLeaveSwaps,
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getCurrentSmartTransactionsEnabled,
getFromTokenInputValue, getFromTokenInputValue,
getMaxSlippage, getMaxSlippage,
setSwapsFromToken, setSwapsFromToken,
@ -113,6 +114,9 @@ export default function AwaitingSwap({
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
); );
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const sensitiveProperties = { const sensitiveProperties = {
token_from: sourceTokenInfo?.symbol, token_from: sourceTokenInfo?.symbol,
token_from_amount: fetchParams?.value, token_from_amount: fetchParams?.value,
@ -124,6 +128,7 @@ export default function AwaitingSwap({
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}; };
const quotesExpiredEvent = useNewMetricEvent({ const quotesExpiredEvent = useNewMetricEvent({

@ -177,11 +177,11 @@ export default function BuildQuote({
const onCloseSmartTransactionsOptInPopover = (e) => { const onCloseSmartTransactionsOptInPopover = (e) => {
e?.preventDefault(); e?.preventDefault();
setSmartTransactionsOptInStatus(false); setSmartTransactionsOptInStatus(false, smartTransactionsOptInStatus);
}; };
const onEnableSmartTransactionsClick = () => const onEnableSmartTransactionsClick = () =>
setSmartTransactionsOptInStatus(true); setSmartTransactionsOptInStatus(true, smartTransactionsOptInStatus);
const fetchParamsFromToken = isSwapsDefaultTokenSymbol( const fetchParamsFromToken = isSwapsDefaultTokenSymbol(
sourceTokenInfo?.symbol, sourceTokenInfo?.symbol,
@ -338,9 +338,7 @@ export default function BuildQuote({
null, // no holderAddress null, // no holderAddress
{ {
blockExplorerUrl: blockExplorerUrl:
rpcPrefs.blockExplorerUrl ?? SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null,
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ??
null,
}, },
); );

@ -129,6 +129,13 @@
} }
} }
} }
&__input {
div {
border: 1px solid var(--color-border-default);
border-left: 0;
}
}
} }
&__open-to-dropdown { &__open-to-dropdown {

@ -27,6 +27,7 @@ import { getURLHostName } from '../../../helpers/utils/util';
import { import {
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getCurrentSmartTransactionsEnabled,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
export default function DropdownSearchList({ export default function DropdownSearchList({
@ -63,6 +64,9 @@ export default function DropdownSearchList({
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
); );
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const tokenImportedEvent = useNewMetricEvent({ const tokenImportedEvent = useNewMetricEvent({
event: 'Token Imported', event: 'Token Imported',
@ -73,6 +77,7 @@ export default function DropdownSearchList({
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}, },
category: 'swaps', category: 'swaps',

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useContext } from 'react'; import React, { useEffect, useRef, useContext, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { import {
Switch, Switch,
@ -37,6 +37,7 @@ import {
getPendingSmartTransactions, getPendingSmartTransactions,
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getCurrentSmartTransactionsEnabled,
getCurrentSmartTransactionsError, getCurrentSmartTransactionsError,
dismissCurrentSmartTransactionsErrorMessage, dismissCurrentSmartTransactionsErrorMessage,
getCurrentSmartTransactionsErrorMessageDismissed, getCurrentSmartTransactionsErrorMessageDismissed,
@ -84,6 +85,7 @@ import {
fetchTopAssets, fetchTopAssets,
getSwapsTokensReceivedFromTxMeta, getSwapsTokensReceivedFromTxMeta,
fetchAggregatorMetadata, fetchAggregatorMetadata,
stxErrorTypes,
} from './swaps.util'; } from './swaps.util';
import AwaitingSignatures from './awaiting-signatures'; import AwaitingSignatures from './awaiting-signatures';
import SmartTransactionStatus from './smart-transaction-status'; import SmartTransactionStatus from './smart-transaction-status';
@ -106,6 +108,7 @@ export default function Swap() {
pathname === SMART_TRANSACTION_STATUS_ROUTE; pathname === SMART_TRANSACTION_STATUS_ROUTE;
const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE; const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE;
const [currentStxErrorTracked, setCurrentStxErrorTracked] = useState(false);
const fetchParams = useSelector(getFetchParams, isEqual); const fetchParams = useSelector(getFetchParams, isEqual);
const { destinationTokenInfo = {} } = fetchParams?.metaData || {}; const { destinationTokenInfo = {} } = fetchParams?.metaData || {};
@ -134,6 +137,9 @@ export default function Swap() {
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
); );
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const currentSmartTransactionsError = useSelector( const currentSmartTransactionsError = useSelector(
getCurrentSmartTransactionsError, getCurrentSmartTransactionsError,
); );
@ -244,6 +250,7 @@ export default function Swap() {
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}, },
}); });
@ -302,22 +309,26 @@ export default function Swap() {
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
stx_error: currentSmartTransactionsError, stx_error: currentSmartTransactionsError,
}, },
}); });
useEffect(() => { useEffect(() => {
if (currentSmartTransactionsError) { if (currentSmartTransactionsError && !currentStxErrorTracked) {
setCurrentStxErrorTracked(true);
errorStxEvent(); errorStxEvent();
} }
}, [errorStxEvent, currentSmartTransactionsError]); }, [errorStxEvent, currentSmartTransactionsError, currentStxErrorTracked]);
if (!isSwapsChain) { if (!isSwapsChain) {
return <Redirect to={{ pathname: DEFAULT_ROUTE }} />; return <Redirect to={{ pathname: DEFAULT_ROUTE }} />;
} }
const isStxNotEnoughFundsError = const isStxNotEnoughFundsError =
currentSmartTransactionsError === 'not_enough_funds'; currentSmartTransactionsError === stxErrorTypes.NOT_ENOUGH_FUNDS;
const isStxRegularTxPendingError =
currentSmartTransactionsError === stxErrorTypes.REGULAR_TX_PENDING;
return ( return (
<div className="swaps"> <div className="swaps">
@ -371,10 +382,20 @@ export default function Swap() {
</div> </div>
) : ( ) : (
<div className="build-quote__token-verification-warning-message"> <div className="build-quote__token-verification-warning-message">
<div className="build-quote__bold"> <button
onClick={() => {
dispatch(dismissCurrentSmartTransactionsErrorMessage());
}}
className="swaps__notification-close-button"
/>
<div className="swaps__notification-title">
{t('stxUnavailable')} {t('stxUnavailable')}
</div> </div>
<div>{t('stxFallbackToNormal')}</div> <div>
{isStxRegularTxPendingError
? t('stxFallbackPendingTx')
: t('stxFallbackUnavailable')}
</div>
</div> </div>
) )
} }
@ -383,16 +404,6 @@ export default function Swap() {
? 'swaps__error-message' ? 'swaps__error-message'
: 'actionable-message--left-aligned actionable-message--warning swaps__error-message' : 'actionable-message--left-aligned actionable-message--warning swaps__error-message'
} }
primaryAction={
isStxNotEnoughFundsError
? null
: {
label: t('dismiss'),
onClick: () =>
dispatch(dismissCurrentSmartTransactionsErrorMessage()),
}
}
withRightButton
/> />
)} )}
<Switch> <Switch>

@ -120,4 +120,28 @@
padding-left: 24px; padding-left: 24px;
flex: 1; flex: 1;
} }
&__notification-close-button {
background-color: transparent;
position: absolute;
right: 0;
top: 2px;
&::after {
position: absolute;
content: '\00D7';
font-size: 29px;
font-weight: 200;
color: var(--color-text-default);
background-color: transparent;
top: 0;
right: 12px;
cursor: pointer;
}
}
&__notification-title {
font-weight: bold;
margin-right: 14px;
}
} }

@ -11,6 +11,7 @@ import {
getQuotesFetchStartTime, getQuotesFetchStartTime,
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getCurrentSmartTransactionsEnabled,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
import { import {
isHardwareWallet, isHardwareWallet,
@ -41,6 +42,9 @@ export default function LoadingSwapsQuotes({
getSmartTransactionsOptInStatus, getSmartTransactionsOptInStatus,
); );
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled,
);
const quotesRequestCancelledEventConfig = { const quotesRequestCancelledEventConfig = {
event: 'Quotes Request Cancelled', event: 'Quotes Request Cancelled',
category: 'swaps', category: 'swaps',
@ -55,6 +59,7 @@ export default function LoadingSwapsQuotes({
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: smartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}, },
}; };

@ -14,7 +14,7 @@ import {
ALIGN_ITEMS, ALIGN_ITEMS,
DISPLAY, DISPLAY,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { smartTransactionsErrorMessages } from '../swaps.util'; import { getTranslatedStxErrorMessage } from '../swaps.util';
export default function SlippageButtons({ export default function SlippageButtons({
onSelect, onSelect,
@ -208,8 +208,9 @@ export default function SlippageButtons({
{currentSmartTransactionsError ? ( {currentSmartTransactionsError ? (
<InfoTooltip <InfoTooltip
position="top" position="top"
contentText={smartTransactionsErrorMessages( contentText={getTranslatedStxErrorMessage(
currentSmartTransactionsError, currentSmartTransactionsError,
t,
)} )}
/> />
) : ( ) : (
@ -219,7 +220,7 @@ export default function SlippageButtons({
<ToggleButton <ToggleButton
value={smartTransactionsOptInStatus} value={smartTransactionsOptInStatus}
onToggle={(value) => { onToggle={(value) => {
setSmartTransactionsOptInStatus(!value); setSmartTransactionsOptInStatus(!value, value);
}} }}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}

@ -45,6 +45,6 @@ describe('SlippageButtons', () => {
expect( expect(
document.querySelector('.slippage-buttons__button-group'), document.querySelector('.slippage-buttons__button-group'),
).toMatchSnapshot(); ).toMatchSnapshot();
expect(getByText('Smart transaction')).toBeInTheDocument(); expect(getByText('Smart Transaction')).toBeInTheDocument();
}); });
}); });

@ -184,6 +184,8 @@ export default function SmartTransactionStatus() {
headerText = t('stxPendingFinalizing'); headerText = t('stxPendingFinalizing');
} else if (timeLeftForPendingStxInSec < 150) { } else if (timeLeftForPendingStxInSec < 150) {
headerText = t('stxPendingPrivatelySubmitting'); headerText = t('stxPendingPrivatelySubmitting');
} else if (cancelSwapLinkClicked) {
headerText = t('stxTryingToCancel');
} }
} }
if (smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS) { if (smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS) {
@ -192,7 +194,11 @@ export default function SmartTransactionStatus() {
description = t('stxSuccessDescription', [destinationTokenInfo.symbol]); description = t('stxSuccessDescription', [destinationTokenInfo.symbol]);
} }
icon = <SuccessIcon />; icon = <SuccessIcon />;
} else if (smartTransactionStatus === 'cancelled_user_cancelled') { } else if (
smartTransactionStatus === 'cancelled_user_cancelled' ||
latestSmartTransaction?.statusMetadata?.minedTx ===
SMART_TRANSACTION_STATUSES.CANCELLED
) {
headerText = t('stxUserCancelled'); headerText = t('stxUserCancelled');
description = t('stxUserCancelledDescription'); description = t('stxUserCancelledDescription');
icon = <CanceledIcon />; icon = <CanceledIcon />;

@ -55,6 +55,7 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH =
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const CACHE_REFRESH_FIVE_MINUTES = 300000; const CACHE_REFRESH_FIVE_MINUTES = 300000;
const USD_CURRENCY_CODE = 'usd';
const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID }; const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID };
@ -514,12 +515,25 @@ export const getFeeForSmartTransaction = ({
conversionRate, conversionRate,
numberOfDecimals: 2, numberOfDecimals: 2,
}); });
let feeInUsd;
if (currentCurrency === USD_CURRENCY_CODE) {
feeInUsd = rawNetworkFees;
} else {
feeInUsd = getValueFromWeiHex({
value: feeInWeiHex,
toCurrency: USD_CURRENCY_CODE,
conversionRate,
numberOfDecimals: 2,
});
}
const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency); const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency);
const chainCurrencySymbolToUse = const chainCurrencySymbolToUse =
nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol; nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol;
return { return {
feeInUsd,
feeInFiat: formattedNetworkFee, feeInFiat: formattedNetworkFee,
feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`, feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`,
rawEthFee: ethFee,
}; };
}; };
@ -564,11 +578,24 @@ export function getRenderableNetworkFeesForQuote({
}); });
const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency); const formattedNetworkFee = formatCurrency(rawNetworkFees, currentCurrency);
let feeInUsd;
if (currentCurrency === USD_CURRENCY_CODE) {
feeInUsd = rawNetworkFees;
} else {
feeInUsd = getValueFromWeiHex({
value: totalWeiCost,
toCurrency: USD_CURRENCY_CODE,
conversionRate,
numberOfDecimals: 2,
});
}
const chainCurrencySymbolToUse = const chainCurrencySymbolToUse =
nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol; nativeCurrencySymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol;
return { return {
rawNetworkFees, rawNetworkFees,
feeInUsd,
rawEthFee: ethFee, rawEthFee: ethFee,
feeInFiat: formattedNetworkFee, feeInFiat: formattedNetworkFee,
feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`, feeInEth: `${ethFee} ${chainCurrencySymbolToUse}`,
@ -903,18 +930,22 @@ export const showRemainingTimeInMinAndSec = (remainingTimeInSec) => {
return `${minutes}:${seconds.toString().padStart(2, '0')}`; return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}; };
export const stxErrorTypes = ['unavailable', 'not_enough_funds']; export const stxErrorTypes = {
UNAVAILABLE: 'unavailable',
const smartTransactionsErrorMap = { NOT_ENOUGH_FUNDS: 'not_enough_funds',
unavailable: 'Smart Transactions are temporarily unavailable.', REGULAR_TX_PENDING: 'regular_tx_pending',
not_enough_funds: 'Not enough funds for a smart transaction.',
}; };
export const smartTransactionsErrorMessages = (errorType) => { export const getTranslatedStxErrorMessage = (errorType, t) => {
return ( switch (errorType) {
smartTransactionsErrorMap[errorType] || case stxErrorTypes.UNAVAILABLE:
smartTransactionsErrorMap.unavailable case stxErrorTypes.REGULAR_TX_PENDING:
); return t('stxErrorUnavailable');
case stxErrorTypes.NOT_ENOUGH_FUNDS:
return t('stxErrorNotEnoughFunds');
default:
return t('stxErrorUnavailable');
}
}; };
export const parseSmartTransactionsError = (errorMessage) => { export const parseSmartTransactionsError = (errorMessage) => {

@ -205,40 +205,6 @@ export default function ViewQuote() {
const swapsRefreshRates = useSelector(getSwapsRefreshStates); const swapsRefreshRates = useSelector(getSwapsRefreshStates);
const unsignedTransaction = usedQuote.trade; const unsignedTransaction = usedQuote.trade;
useEffect(() => {
if (currentSmartTransactionsEnabled && smartTransactionsOptInStatus) {
const unsignedTx = {
from: unsignedTransaction.from,
to: unsignedTransaction.to,
value: unsignedTransaction.value,
data: unsignedTransaction.data,
gas: unsignedTransaction.gas,
chainId,
};
intervalId = setInterval(() => {
dispatch(
estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams),
);
}, swapsRefreshRates.stxGetTransactionsRefreshTime);
dispatch(estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams));
} else if (intervalId) {
clearInterval(intervalId);
}
return () => clearInterval(intervalId);
// eslint-disable-next-line
}, [
dispatch,
currentSmartTransactionsEnabled,
smartTransactionsOptInStatus,
unsignedTransaction.data,
unsignedTransaction.from,
unsignedTransaction.value,
unsignedTransaction.gas,
unsignedTransaction.to,
chainId,
swapsRefreshRates.stxGetTransactionsRefreshTime,
]);
let gasFeeInputs; let gasFeeInputs;
if (networkAndAccountSupports1559) { if (networkAndAccountSupports1559) {
// For Swaps we want to get 'high' estimations by default. // For Swaps we want to get 'high' estimations by default.
@ -252,6 +218,17 @@ export default function ViewQuote() {
const fetchParamsSourceToken = fetchParams?.sourceToken; const fetchParamsSourceToken = fetchParams?.sourceToken;
const additionalTrackingParams = {
reg_tx_fee_in_usd: undefined,
reg_tx_fee_in_eth: undefined,
reg_tx_max_fee_in_usd: undefined,
reg_tx_max_fee_in_eth: undefined,
stx_fee_in_usd: undefined,
stx_fee_in_eth: undefined,
stx_max_fee_in_usd: undefined,
stx_max_fee_in_eth: undefined,
};
const usedGasLimit = const usedGasLimit =
usedQuote?.gasEstimateWithRefund || usedQuote?.gasEstimateWithRefund ||
`0x${decimalToHex(usedQuote?.averageGas || 0)}`; `0x${decimalToHex(usedQuote?.averageGas || 0)}`;
@ -266,7 +243,7 @@ export default function ViewQuote() {
const nonCustomMaxGasLimit = usedQuote?.gasEstimate const nonCustomMaxGasLimit = usedQuote?.gasEstimate
? usedGasLimitWithMultiplier ? usedGasLimitWithMultiplier
: `0x${decimalToHex(usedQuote?.maxGas || 0)}`; : `0x${decimalToHex(usedQuote?.maxGas || 0)}`;
let maxGasLimit = customMaxGas || nonCustomMaxGasLimit; const maxGasLimit = customMaxGas || nonCustomMaxGasLimit;
let maxFeePerGas; let maxFeePerGas;
let maxPriorityFeePerGas; let maxPriorityFeePerGas;
@ -289,17 +266,6 @@ export default function ViewQuote() {
); );
} }
// Smart Transactions gas fees.
if (
currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus &&
smartTransactionEstimatedGas?.txData
) {
maxGasLimit = `0x${decimalToHex(
smartTransactionEstimatedGas?.txData.gasLimit || 0,
)}`;
}
const gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); const gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice);
const { tokensWithBalances } = useTokenTracker(swapsTokens, true); const { tokensWithBalances } = useTokenTracker(swapsTokens, true);
@ -374,7 +340,12 @@ export default function ViewQuote() {
sourceTokenIconUrl, sourceTokenIconUrl,
} = renderableDataForUsedQuote; } = renderableDataForUsedQuote;
let { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({ let {
feeInFiat,
feeInEth,
rawEthFee,
feeInUsd,
} = getRenderableNetworkFeesForQuote({
tradeGas: usedGasLimit, tradeGas: usedGasLimit,
approveGas, approveGas,
gasPrice: networkAndAccountSupports1559 gasPrice: networkAndAccountSupports1559
@ -388,6 +359,8 @@ export default function ViewQuote() {
chainId, chainId,
nativeCurrencySymbol, nativeCurrencySymbol,
}); });
additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd);
additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee);
const renderableMaxFees = getRenderableNetworkFeesForQuote({ const renderableMaxFees = getRenderableNetworkFeesForQuote({
tradeGas: maxGasLimit, tradeGas: maxGasLimit,
@ -401,8 +374,15 @@ export default function ViewQuote() {
chainId, chainId,
nativeCurrencySymbol, nativeCurrencySymbol,
}); });
let { feeInFiat: maxFeeInFiat, feeInEth: maxFeeInEth } = renderableMaxFees; let {
feeInFiat: maxFeeInFiat,
feeInEth: maxFeeInEth,
rawEthFee: maxRawEthFee,
feeInUsd: maxFeeInUsd,
} = renderableMaxFees;
const { nonGasFee } = renderableMaxFees; const { nonGasFee } = renderableMaxFees;
additionalTrackingParams.reg_tx_max_fee_in_usd = Number(maxFeeInUsd);
additionalTrackingParams.reg_tx_max_fee_in_eth = Number(maxRawEthFee);
if ( if (
currentSmartTransactionsEnabled && currentSmartTransactionsEnabled &&
@ -413,16 +393,22 @@ export default function ViewQuote() {
smartTransactionEstimatedGas.txData.feeEstimate + smartTransactionEstimatedGas.txData.feeEstimate +
(smartTransactionEstimatedGas.approvalTxData?.feeEstimate || 0); (smartTransactionEstimatedGas.approvalTxData?.feeEstimate || 0);
const stxMaxFeeInWeiDec = stxEstimatedFeeInWeiDec * 2; const stxMaxFeeInWeiDec = stxEstimatedFeeInWeiDec * 2;
({ feeInFiat, feeInEth } = getFeeForSmartTransaction({ ({ feeInFiat, feeInEth, rawEthFee, feeInUsd } = getFeeForSmartTransaction({
chainId, chainId,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
nativeCurrencySymbol, nativeCurrencySymbol,
feeInWeiDec: stxEstimatedFeeInWeiDec, feeInWeiDec: stxEstimatedFeeInWeiDec,
})); }));
additionalTrackingParams.stx_fee_in_usd = Number(feeInUsd);
additionalTrackingParams.stx_fee_in_eth = Number(rawEthFee);
additionalTrackingParams.estimated_gas =
smartTransactionEstimatedGas.txData.gasLimit;
({ ({
feeInFiat: maxFeeInFiat, feeInFiat: maxFeeInFiat,
feeInEth: maxFeeInEth, feeInEth: maxFeeInEth,
rawEthFee: maxRawEthFee,
feeInUsd: maxFeeInUsd,
} = getFeeForSmartTransaction({ } = getFeeForSmartTransaction({
chainId, chainId,
currentCurrency, currentCurrency,
@ -430,6 +416,8 @@ export default function ViewQuote() {
nativeCurrencySymbol, nativeCurrencySymbol,
feeInWeiDec: stxMaxFeeInWeiDec, feeInWeiDec: stxMaxFeeInWeiDec,
})); }));
additionalTrackingParams.stx_max_fee_in_usd = Number(maxFeeInUsd);
additionalTrackingParams.stx_max_fee_in_eth = Number(maxRawEthFee);
} }
const tokenCost = new BigNumber(usedQuote.sourceAmount); const tokenCost = new BigNumber(usedQuote.sourceAmount);
@ -520,7 +508,7 @@ export default function ViewQuote() {
available_quotes: numberOfQuotes, available_quotes: numberOfQuotes,
is_hardware_wallet: hardwareWalletUsed, is_hardware_wallet: hardwareWalletUsed,
hardware_wallet_type: hardwareWalletType, hardware_wallet_type: hardwareWalletType,
stx_enabled: currentSmartTransactionsEnabled, stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: smartTransactionsOptInStatus, stx_user_opt_in: smartTransactionsOptInStatus,
}; };
@ -782,6 +770,55 @@ export default function ViewQuote() {
const isShowingWarning = const isShowingWarning =
showInsufficientWarning || shouldShowPriceDifferenceWarning; showInsufficientWarning || shouldShowPriceDifferenceWarning;
const isSwapButtonDisabled =
submitClicked ||
balanceError ||
tokenBalanceUnavailable ||
disableSubmissionDueToPriceWarning ||
(networkAndAccountSupports1559 && baseAndPriorityFeePerGas === undefined) ||
(!networkAndAccountSupports1559 &&
(gasPrice === null || gasPrice === undefined)) ||
(currentSmartTransactionsEnabled && currentSmartTransactionsError);
useEffect(() => {
if (
currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus &&
!isSwapButtonDisabled
) {
const unsignedTx = {
from: unsignedTransaction.from,
to: unsignedTransaction.to,
value: unsignedTransaction.value,
data: unsignedTransaction.data,
gas: unsignedTransaction.gas,
chainId,
};
intervalId = setInterval(() => {
dispatch(
estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams),
);
}, swapsRefreshRates.stxGetTransactionsRefreshTime);
dispatch(estimateSwapsSmartTransactionsGas(unsignedTx, approveTxParams));
} else if (intervalId) {
clearInterval(intervalId);
}
return () => clearInterval(intervalId);
// eslint-disable-next-line
}, [
dispatch,
currentSmartTransactionsEnabled,
smartTransactionsOptInStatus,
unsignedTransaction.data,
unsignedTransaction.from,
unsignedTransaction.value,
unsignedTransaction.gas,
unsignedTransaction.to,
chainId,
swapsRefreshRates.stxGetTransactionsRefreshTime,
isSwapButtonDisabled,
]);
const onCloseEditGasPopover = () => { const onCloseEditGasPopover = () => {
setShowEditGasPopover(false); setShowEditGasPopover(false);
}; };
@ -967,10 +1004,17 @@ export default function ViewQuote() {
unsignedTransaction, unsignedTransaction,
metaMetricsEvent, metaMetricsEvent,
history, history,
additionalTrackingParams,
}), }),
); );
} else { } else {
dispatch(signAndSendTransactions(history, metaMetricsEvent)); dispatch(
signAndSendTransactions(
history,
metaMetricsEvent,
additionalTrackingParams,
),
);
} }
} else if (destinationToken.symbol === defaultSwapsToken.symbol) { } else if (destinationToken.symbol === defaultSwapsToken.symbol) {
history.push(DEFAULT_ROUTE); history.push(DEFAULT_ROUTE);
@ -986,17 +1030,7 @@ export default function ViewQuote() {
: t('swap') : t('swap')
} }
hideCancel hideCancel
disabled={ disabled={isSwapButtonDisabled}
submitClicked ||
balanceError ||
tokenBalanceUnavailable ||
disableSubmissionDueToPriceWarning ||
(networkAndAccountSupports1559 &&
baseAndPriorityFeePerGas === undefined) ||
(!networkAndAccountSupports1559 &&
(gasPrice === null || gasPrice === undefined)) ||
(currentSmartTransactionsEnabled && currentSmartTransactionsError)
}
className={isShowingWarning && 'view-quote__thin-swaps-footer'} className={isShowingWarning && 'view-quote__thin-swaps-footer'}
showTopBorder showTopBorder
/> />

@ -3230,7 +3230,10 @@ export async function setWeb3ShimUsageAlertDismissed(origin) {
} }
// Smart Transactions Controller // Smart Transactions Controller
export async function setSmartTransactionsOptInStatus(optInState) { export async function setSmartTransactionsOptInStatus(
optInState,
prevOptInState,
) {
trackMetaMetricsEvent({ trackMetaMetricsEvent({
event: 'STX OptIn', event: 'STX OptIn',
category: 'swaps', category: 'swaps',
@ -3238,6 +3241,7 @@ export async function setSmartTransactionsOptInStatus(optInState) {
stx_enabled: true, stx_enabled: true,
current_stx_enabled: true, current_stx_enabled: true,
stx_user_opt_in: optInState, stx_user_opt_in: optInState,
stx_prev_user_opt_in: prevOptInState,
}, },
}); });
await promisifiedBackground.setSmartTransactionsOptInStatus(optInState); await promisifiedBackground.setSmartTransactionsOptInStatus(optInState);

Loading…
Cancel
Save