|
|
|
import { createSlice } from '@reduxjs/toolkit'
|
|
|
|
import BigNumber from 'bignumber.js'
|
|
|
|
import log from 'loglevel'
|
|
|
|
|
|
|
|
import {
|
|
|
|
loadLocalStorageData,
|
|
|
|
saveLocalStorageData,
|
|
|
|
} from '../../../lib/local-storage-helpers'
|
|
|
|
import {
|
|
|
|
addToken,
|
|
|
|
addUnapprovedTransaction,
|
|
|
|
fetchAndSetQuotes,
|
|
|
|
forceUpdateMetamaskState,
|
|
|
|
resetSwapsPostFetchState,
|
|
|
|
setBackgroundSwapRouteState,
|
|
|
|
setInitialGasEstimate,
|
|
|
|
setSwapsErrorKey,
|
|
|
|
setSwapsTxGasPrice,
|
|
|
|
setApproveTxId,
|
|
|
|
setTradeTxId,
|
|
|
|
stopPollingForQuotes,
|
|
|
|
updateAndApproveTx,
|
|
|
|
updateTransaction,
|
|
|
|
resetBackgroundSwapsState,
|
|
|
|
setSwapsLiveness,
|
|
|
|
setSelectedQuoteAggId,
|
|
|
|
setSwapsTxGasLimit,
|
|
|
|
} from '../../store/actions'
|
|
|
|
import {
|
|
|
|
AWAITING_SWAP_ROUTE,
|
|
|
|
BUILD_QUOTE_ROUTE,
|
|
|
|
LOADING_QUOTES_ROUTE,
|
|
|
|
SWAPS_ERROR_ROUTE,
|
|
|
|
SWAPS_MAINTENANCE_ROUTE,
|
|
|
|
} from '../../helpers/constants/routes'
|
|
|
|
import {
|
|
|
|
fetchSwapsFeatureLiveness,
|
|
|
|
fetchSwapsGasPrices,
|
|
|
|
} from '../../pages/swaps/swaps.util'
|
|
|
|
import { calcGasTotal } from '../../pages/send/send.utils'
|
|
|
|
import {
|
|
|
|
decimalToHex,
|
|
|
|
getValueFromWeiHex,
|
|
|
|
hexMax,
|
|
|
|
decGWEIToHexWEI,
|
|
|
|
hexToDecimal,
|
|
|
|
hexWEIToDecGWEI,
|
|
|
|
} from '../../helpers/utils/conversions.util'
|
|
|
|
import { conversionLessThan } from '../../helpers/utils/conversion-util'
|
|
|
|
import { calcTokenAmount } from '../../helpers/utils/token-util'
|
|
|
|
import {
|
|
|
|
getSelectedAccount,
|
|
|
|
getTokenExchangeRates,
|
|
|
|
getUSDConversionRate,
|
|
|
|
} from '../../selectors'
|
|
|
|
import {
|
|
|
|
ERROR_FETCHING_QUOTES,
|
|
|
|
QUOTES_NOT_AVAILABLE_ERROR,
|
|
|
|
ETH_SWAPS_TOKEN_OBJECT,
|
|
|
|
SWAP_FAILED_ERROR,
|
|
|
|
SWAPS_FETCH_ORDER_CONFLICT,
|
|
|
|
} from '../../helpers/constants/swaps'
|
|
|
|
import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'
|
|
|
|
|
|
|
|
const GAS_PRICES_LOADING_STATES = {
|
|
|
|
INITIAL: 'INITIAL',
|
|
|
|
LOADING: 'LOADING',
|
|
|
|
FAILED: 'FAILED',
|
|
|
|
COMPLETED: 'COMPLETED',
|
|
|
|
}
|
|
|
|
|
|
|
|
const initialState = {
|
|
|
|
aggregatorMetadata: null,
|
|
|
|
approveTxId: null,
|
|
|
|
balanceError: false,
|
|
|
|
fetchingQuotes: false,
|
|
|
|
fromToken: null,
|
|
|
|
quotesFetchStartTime: null,
|
|
|
|
topAssets: {},
|
|
|
|
toToken: null,
|
|
|
|
customGas: {
|
|
|
|
price: null,
|
|
|
|
limit: null,
|
|
|
|
loading: GAS_PRICES_LOADING_STATES.INITIAL,
|
|
|
|
priceEstimates: {},
|
|
|
|
priceEstimatesLastRetrieved: 0,
|
|
|
|
fallBackPrice: null,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
const slice = createSlice({
|
|
|
|
name: 'swaps',
|
|
|
|
initialState,
|
|
|
|
reducers: {
|
|
|
|
clearSwapsState: () => initialState,
|
|
|
|
navigatedBackToBuildQuote: (state) => {
|
|
|
|
state.approveTxId = null
|
|
|
|
state.balanceError = false
|
|
|
|
state.fetchingQuotes = false
|
|
|
|
state.customGas.limit = null
|
|
|
|
state.customGas.price = null
|
|
|
|
},
|
|
|
|
retriedGetQuotes: (state) => {
|
|
|
|
state.approveTxId = null
|
|
|
|
state.balanceError = false
|
|
|
|
state.fetchingQuotes = false
|
|
|
|
},
|
|
|
|
setAggregatorMetadata: (state, action) => {
|
|
|
|
state.aggregatorMetadata = action.payload
|
|
|
|
},
|
|
|
|
setBalanceError: (state, action) => {
|
|
|
|
state.balanceError = action.payload
|
|
|
|
},
|
|
|
|
setFetchingQuotes: (state, action) => {
|
|
|
|
state.fetchingQuotes = action.payload
|
|
|
|
},
|
|
|
|
setFromToken: (state, action) => {
|
|
|
|
state.fromToken = action.payload
|
|
|
|
},
|
|
|
|
setQuotesFetchStartTime: (state, action) => {
|
|
|
|
state.quotesFetchStartTime = action.payload
|
|
|
|
},
|
|
|
|
setTopAssets: (state, action) => {
|
|
|
|
state.topAssets = action.payload
|
|
|
|
},
|
|
|
|
setToToken: (state, action) => {
|
|
|
|
state.toToken = action.payload
|
|
|
|
},
|
|
|
|
swapCustomGasModalClosed: (state) => {
|
|
|
|
state.customGas.price = null
|
|
|
|
state.customGas.limit = null
|
|
|
|
},
|
|
|
|
swapCustomGasModalPriceEdited: (state, action) => {
|
|
|
|
state.customGas.price = action.payload
|
|
|
|
},
|
|
|
|
swapCustomGasModalLimitEdited: (state, action) => {
|
|
|
|
state.customGas.limit = action.payload
|
|
|
|
},
|
|
|
|
swapGasPriceEstimatesFetchStarted: (state) => {
|
|
|
|
state.customGas.loading = GAS_PRICES_LOADING_STATES.LOADING
|
|
|
|
},
|
|
|
|
swapGasPriceEstimatesFetchFailed: (state) => {
|
|
|
|
state.customGas.loading = GAS_PRICES_LOADING_STATES.FAILED
|
|
|
|
},
|
|
|
|
swapGasPriceEstimatesFetchCompleted: (state, action) => {
|
|
|
|
state.customGas.priceEstimates = action.payload.priceEstimates
|
|
|
|
state.customGas.loading = GAS_PRICES_LOADING_STATES.COMPLETED
|
|
|
|
state.customGas.priceEstimatesLastRetrieved =
|
|
|
|
action.payload.priceEstimatesLastRetrieved
|
|
|
|
},
|
|
|
|
retrievedFallbackSwapsGasPrice: (state, action) => {
|
|
|
|
state.customGas.fallBackPrice = action.payload
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
const { actions, reducer } = slice
|
|
|
|
|
|
|
|
export default reducer
|
|
|
|
|
|
|
|
// Selectors
|
|
|
|
|
|
|
|
export const getAggregatorMetadata = (state) => state.swaps.aggregatorMetadata
|
|
|
|
|
|
|
|
export const getBalanceError = (state) => state.swaps.balanceError
|
|
|
|
|
|
|
|
export const getFromToken = (state) => state.swaps.fromToken
|
|
|
|
|
|
|
|
export const getTopAssets = (state) => state.swaps.topAssets
|
|
|
|
|
|
|
|
export const getToToken = (state) => state.swaps.toToken
|
|
|
|
|
|
|
|
export const getFetchingQuotes = (state) => state.swaps.fetchingQuotes
|
|
|
|
|
|
|
|
export const getQuotesFetchStartTime = (state) =>
|
|
|
|
state.swaps.quotesFetchStartTime
|
|
|
|
|
|
|
|
export const getSwapsCustomizationModalPrice = (state) =>
|
|
|
|
state.swaps.customGas.price
|
|
|
|
|
|
|
|
export const getSwapsCustomizationModalLimit = (state) =>
|
|
|
|
state.swaps.customGas.limit
|
|
|
|
|
|
|
|
export const swapGasPriceEstimateIsLoading = (state) =>
|
|
|
|
state.swaps.customGas.loading === GAS_PRICES_LOADING_STATES.LOADING
|
|
|
|
|
|
|
|
export const swapGasEstimateLoadingHasFailed = (state) =>
|
|
|
|
state.swaps.customGas.loading === GAS_PRICES_LOADING_STATES.INITIAL
|
|
|
|
|
|
|
|
export const getSwapGasPriceEstimateData = (state) =>
|
|
|
|
state.swaps.customGas.priceEstimates
|
|
|
|
|
|
|
|
export const getSwapsPriceEstimatesLastRetrieved = (state) =>
|
|
|
|
state.swaps.customGas.priceEstimatesLastRetrieved
|
|
|
|
|
|
|
|
export const getSwapsFallbackGasPrice = (state) =>
|
|
|
|
state.swaps.customGas.fallBackPrice
|
|
|
|
|
|
|
|
export function shouldShowCustomPriceTooLowWarning(state) {
|
|
|
|
const { average } = getSwapGasPriceEstimateData(state)
|
|
|
|
|
|
|
|
const customGasPrice = getSwapsCustomizationModalPrice(state)
|
|
|
|
|
|
|
|
if (!customGasPrice || average === undefined) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const customPriceRisksSwapFailure = conversionLessThan(
|
|
|
|
{
|
|
|
|
value: customGasPrice,
|
|
|
|
fromNumericBase: 'hex',
|
|
|
|
fromDenomination: 'WEI',
|
|
|
|
toDenomination: 'GWEI',
|
|
|
|
},
|
|
|
|
{ value: average, fromNumericBase: 'dec' },
|
|
|
|
)
|
|
|
|
|
|
|
|
return customPriceRisksSwapFailure
|
|
|
|
}
|
|
|
|
|
|
|
|
// Background selectors
|
|
|
|
|
|
|
|
const getSwapsState = (state) => state.metamask.swapsState
|
|
|
|
|
|
|
|
export const getSwapsFeatureLiveness = (state) =>
|
|
|
|
state.metamask.swapsState.swapsFeatureIsLive
|
|
|
|
|
|
|
|
export const getBackgroundSwapRouteState = (state) =>
|
|
|
|
state.metamask.swapsState.routeState
|
|
|
|
|
|
|
|
export const getCustomSwapsGas = (state) =>
|
|
|
|
state.metamask.swapsState.customMaxGas
|
|
|
|
|
|
|
|
export const getCustomSwapsGasPrice = (state) =>
|
|
|
|
state.metamask.swapsState.customGasPrice
|
|
|
|
|
|
|
|
export const getFetchParams = (state) => state.metamask.swapsState.fetchParams
|
|
|
|
|
|
|
|
export const getQuotes = (state) => state.metamask.swapsState.quotes
|
|
|
|
|
|
|
|
export const getQuotesLastFetched = (state) =>
|
|
|
|
state.metamask.swapsState.quotesLastFetched
|
|
|
|
|
|
|
|
export const getSelectedQuote = (state) => {
|
|
|
|
const { selectedAggId, quotes } = getSwapsState(state)
|
|
|
|
return quotes[selectedAggId]
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getSwapsErrorKey = (state) => getSwapsState(state)?.errorKey
|
|
|
|
|
|
|
|
export const getShowQuoteLoadingScreen = (state) =>
|
|
|
|
state.swaps.showQuoteLoadingScreen
|
|
|
|
|
|
|
|
export const getSwapsTokens = (state) => state.metamask.swapsState.tokens
|
|
|
|
|
|
|
|
export const getSwapsWelcomeMessageSeenStatus = (state) =>
|
|
|
|
state.metamask.swapsWelcomeMessageHasBeenShown
|
|
|
|
|
|
|
|
export const getTopQuote = (state) => {
|
|
|
|
const { topAggId, quotes } = getSwapsState(state)
|
|
|
|
return quotes[topAggId]
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getApproveTxId = (state) => state.metamask.swapsState.approveTxId
|
|
|
|
|
|
|
|
export const getTradeTxId = (state) => state.metamask.swapsState.tradeTxId
|
|
|
|
|
|
|
|
export const getUsedQuote = (state) =>
|
|
|
|
getSelectedQuote(state) || getTopQuote(state)
|
|
|
|
|
|
|
|
// Compound selectors
|
|
|
|
|
|
|
|
export const getDestinationTokenInfo = (state) =>
|
|
|
|
getFetchParams(state)?.metaData?.destinationTokenInfo
|
|
|
|
|
|
|
|
export const getUsedSwapsGasPrice = (state) =>
|
|
|
|
getCustomSwapsGasPrice(state) || getSwapsFallbackGasPrice(state)
|
|
|
|
|
|
|
|
export const getApproveTxParams = (state) => {
|
|
|
|
const { approvalNeeded } = getSelectedQuote(state) || getTopQuote(state) || {}
|
|
|
|
|
|
|
|
if (!approvalNeeded) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
const data = getSwapsState(state)?.customApproveTxData || approvalNeeded.data
|
|
|
|
|
|
|
|
const gasPrice = getCustomSwapsGasPrice(state) || approvalNeeded.gasPrice
|
|
|
|
return { ...approvalNeeded, gasPrice, data }
|
|
|
|
}
|
|
|
|
|
|
|
|
// Actions / action-creators
|
|
|
|
|
|
|
|
const {
|
|
|
|
clearSwapsState,
|
|
|
|
navigatedBackToBuildQuote,
|
|
|
|
retriedGetQuotes,
|
|
|
|
swapGasPriceEstimatesFetchCompleted,
|
|
|
|
swapGasPriceEstimatesFetchStarted,
|
|
|
|
swapGasPriceEstimatesFetchFailed,
|
|
|
|
setAggregatorMetadata,
|
|
|
|
setBalanceError,
|
|
|
|
setFetchingQuotes,
|
|
|
|
setFromToken,
|
|
|
|
setQuotesFetchStartTime,
|
|
|
|
setTopAssets,
|
|
|
|
setToToken,
|
|
|
|
swapCustomGasModalPriceEdited,
|
|
|
|
swapCustomGasModalLimitEdited,
|
|
|
|
retrievedFallbackSwapsGasPrice,
|
|
|
|
swapCustomGasModalClosed,
|
|
|
|
} = actions
|
|
|
|
|
|
|
|
export {
|
|
|
|
clearSwapsState,
|
|
|
|
setAggregatorMetadata,
|
|
|
|
setBalanceError,
|
|
|
|
setFetchingQuotes,
|
|
|
|
setFromToken as setSwapsFromToken,
|
|
|
|
setQuotesFetchStartTime as setSwapQuotesFetchStartTime,
|
|
|
|
setTopAssets,
|
|
|
|
setToToken as setSwapToToken,
|
|
|
|
swapCustomGasModalPriceEdited,
|
|
|
|
swapCustomGasModalLimitEdited,
|
|
|
|
swapCustomGasModalClosed,
|
|
|
|
}
|
|
|
|
|
|
|
|
export const navigateBackToBuildQuote = (history) => {
|
|
|
|
return async (dispatch) => {
|
|
|
|
// TODO: Ensure any fetch in progress is cancelled
|
|
|
|
await dispatch(resetSwapsPostFetchState())
|
|
|
|
dispatch(navigatedBackToBuildQuote())
|
|
|
|
|
|
|
|
history.push(BUILD_QUOTE_ROUTE)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const prepareForRetryGetQuotes = () => {
|
|
|
|
return async (dispatch) => {
|
|
|
|
// TODO: Ensure any fetch in progress is cancelled
|
|
|
|
await dispatch(resetSwapsPostFetchState())
|
|
|
|
dispatch(retriedGetQuotes())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const prepareToLeaveSwaps = () => {
|
|
|
|
return async (dispatch) => {
|
|
|
|
dispatch(clearSwapsState())
|
|
|
|
await dispatch(resetBackgroundSwapsState())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const swapsQuoteSelected = (aggId) => {
|
|
|
|
return (dispatch) => {
|
|
|
|
dispatch(swapCustomGasModalLimitEdited(null))
|
|
|
|
dispatch(setSelectedQuoteAggId(aggId))
|
|
|
|
dispatch(setSwapsTxGasLimit(''))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const fetchAndSetSwapsGasPriceInfo = () => {
|
|
|
|
return async (dispatch) => {
|
|
|
|
const basicEstimates = await dispatch(fetchMetaSwapsGasPriceEstimates())
|
|
|
|
|
|
|
|
if (basicEstimates?.fast) {
|
|
|
|
dispatch(setSwapsTxGasPrice(decGWEIToHexWEI(basicEstimates.fast)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const fetchQuotesAndSetQuoteState = (
|
|
|
|
history,
|
|
|
|
inputValue,
|
|
|
|
maxSlippage,
|
|
|
|
metaMetricsEvent,
|
|
|
|
) => {
|
|
|
|
return async (dispatch, getState) => {
|
|
|
|
let swapsFeatureIsLive = false
|
|
|
|
try {
|
|
|
|
swapsFeatureIsLive = await fetchSwapsFeatureLiveness()
|
|
|
|
} catch (error) {
|
|
|
|
log.error('Failed to fetch Swaps liveness, defaulting to false.', error)
|
|
|
|
}
|
|
|
|
await dispatch(setSwapsLiveness(swapsFeatureIsLive))
|
|
|
|
|
|
|
|
if (!swapsFeatureIsLive) {
|
|
|
|
await history.push(SWAPS_MAINTENANCE_ROUTE)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const state = getState()
|
|
|
|
const fetchParams = getFetchParams(state)
|
|
|
|
const selectedAccount = getSelectedAccount(state)
|
|
|
|
const balanceError = getBalanceError(state)
|
|
|
|
const fetchParamsFromToken =
|
|
|
|
fetchParams?.metaData?.sourceTokenInfo?.symbol === 'ETH'
|
|
|
|
? {
|
|
|
|
...ETH_SWAPS_TOKEN_OBJECT,
|
|
|
|
string: getValueFromWeiHex({
|
|
|
|
value: selectedAccount.balance,
|
|
|
|
numberOfDecimals: 4,
|
|
|
|
toDenomination: 'ETH',
|
|
|
|
}),
|
|
|
|
balance: hexToDecimal(selectedAccount.balance),
|
|
|
|
}
|
|
|
|
: fetchParams?.metaData?.sourceTokenInfo
|
|
|
|
const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {}
|
|
|
|
const selectedToToken =
|
|
|
|
getToToken(state) || fetchParams?.metaData?.destinationTokenInfo || {}
|
|
|
|
const {
|
|
|
|
address: fromTokenAddress,
|
|
|
|
symbol: fromTokenSymbol,
|
|
|
|
decimals: fromTokenDecimals,
|
|
|
|
iconUrl: fromTokenIconUrl,
|
|
|
|
balance: fromTokenBalance,
|
|
|
|
} = selectedFromToken
|
|
|
|
const {
|
|
|
|
address: toTokenAddress,
|
|
|
|
symbol: toTokenSymbol,
|
|
|
|
decimals: toTokenDecimals,
|
|
|
|
iconUrl: toTokenIconUrl,
|
|
|
|
} = selectedToToken
|
|
|
|
await dispatch(setBackgroundSwapRouteState('loading'))
|
|
|
|
history.push(LOADING_QUOTES_ROUTE)
|
|
|
|
dispatch(setFetchingQuotes(true))
|
|
|
|
|
|
|
|
const contractExchangeRates = getTokenExchangeRates(state)
|
|
|
|
|
|
|
|
let destinationTokenAddedForSwap = false
|
|
|
|
if (toTokenSymbol !== 'ETH' && !contractExchangeRates[toTokenAddress]) {
|
|
|
|
destinationTokenAddedForSwap = true
|
|
|
|
await dispatch(
|
|
|
|
addToken(
|
|
|
|
toTokenAddress,
|
|
|
|
toTokenSymbol,
|
|
|
|
toTokenDecimals,
|
|
|
|
toTokenIconUrl,
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
fromTokenSymbol !== 'ETH' &&
|
|
|
|
!contractExchangeRates[fromTokenAddress] &&
|
|
|
|
fromTokenBalance &&
|
|
|
|
new BigNumber(fromTokenBalance, 16).gt(0)
|
|
|
|
) {
|
|
|
|
dispatch(
|
|
|
|
addToken(
|
|
|
|
fromTokenAddress,
|
|
|
|
fromTokenSymbol,
|
|
|
|
fromTokenDecimals,
|
|
|
|
fromTokenIconUrl,
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const swapsTokens = getSwapsTokens(state)
|
|
|
|
|
|
|
|
const sourceTokenInfo =
|
|
|
|
swapsTokens?.find(({ address }) => address === fromTokenAddress) ||
|
|
|
|
selectedFromToken
|
|
|
|
const destinationTokenInfo =
|
|
|
|
swapsTokens?.find(({ address }) => address === toTokenAddress) ||
|
|
|
|
selectedToToken
|
|
|
|
|
|
|
|
dispatch(setFromToken(selectedFromToken))
|
|
|
|
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'Quotes Requested',
|
|
|
|
category: 'swaps',
|
|
|
|
})
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'Quotes Requested',
|
|
|
|
category: 'swaps',
|
|
|
|
excludeMetaMetricsId: true,
|
|
|
|
properties: {
|
|
|
|
token_from: fromTokenSymbol,
|
|
|
|
token_from_amount: String(inputValue),
|
|
|
|
token_to: toTokenSymbol,
|
|
|
|
request_type: balanceError ? 'Quote' : 'Order',
|
|
|
|
slippage: maxSlippage,
|
|
|
|
custom_slippage: maxSlippage !== 2,
|
|
|
|
anonymizedData: true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
try {
|
|
|
|
const fetchStartTime = Date.now()
|
|
|
|
dispatch(setQuotesFetchStartTime(fetchStartTime))
|
|
|
|
|
|
|
|
const fetchAndSetQuotesPromise = dispatch(
|
|
|
|
fetchAndSetQuotes(
|
|
|
|
{
|
|
|
|
slippage: maxSlippage,
|
|
|
|
sourceToken: fromTokenAddress,
|
|
|
|
destinationToken: toTokenAddress,
|
|
|
|
value: inputValue,
|
|
|
|
fromAddress: selectedAccount.address,
|
|
|
|
destinationTokenAddedForSwap,
|
|
|
|
balanceError,
|
|
|
|
sourceDecimals: fromTokenDecimals,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
sourceTokenInfo,
|
|
|
|
destinationTokenInfo,
|
|
|
|
accountBalance: selectedAccount.balance,
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
const gasPriceFetchPromise = dispatch(fetchAndSetSwapsGasPriceInfo())
|
|
|
|
|
|
|
|
const [[fetchedQuotes, selectedAggId]] = await Promise.all([
|
|
|
|
fetchAndSetQuotesPromise,
|
|
|
|
gasPriceFetchPromise,
|
|
|
|
])
|
|
|
|
|
|
|
|
if (Object.values(fetchedQuotes)?.length === 0) {
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'No Quotes Available',
|
|
|
|
category: 'swaps',
|
|
|
|
})
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'No Quotes Available',
|
|
|
|
category: 'swaps',
|
|
|
|
excludeMetaMetricsId: true,
|
|
|
|
properties: {
|
|
|
|
token_from: fromTokenSymbol,
|
|
|
|
token_from_amount: String(inputValue),
|
|
|
|
token_to: toTokenSymbol,
|
|
|
|
request_type: balanceError ? 'Quote' : 'Order',
|
|
|
|
slippage: maxSlippage,
|
|
|
|
custom_slippage: maxSlippage !== 2,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR))
|
|
|
|
} else {
|
|
|
|
const newSelectedQuote = fetchedQuotes[selectedAggId]
|
|
|
|
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'Quotes Received',
|
|
|
|
category: 'swaps',
|
|
|
|
})
|
|
|
|
metaMetricsEvent({
|
|
|
|
event: 'Quotes Received',
|
|
|
|
category: 'swaps',
|
|
|
|
excludeMetaMetricsId: true,
|
|
|
|
properties: {
|
|
|
|
token_from: fromTokenSymbol,
|
|
|
|
token_from_amount: String(inputValue),
|
|
|
|
token_to: toTokenSymbol,
|
|
|
|
token_to_amount: calcTokenAmount(
|
|
|
|
newSelectedQuote.destinationAmount,
|
|
|
|
newSelectedQuote.decimals || 18,
|
|
|
|
),
|
|
|
|
request_type: balanceError ? 'Quote' : 'Order',
|
|
|
|
slippage: maxSlippage,
|
|
|
|
custom_slippage: maxSlippage !== 2,
|
|
|
|
response_time: Date.now() - fetchStartTime,
|
|
|
|
best_quote_source: newSelectedQuote.aggregator,
|
|
|
|
available_quotes: Object.values(fetchedQuotes)?.length,
|
|
|
|
anonymizedData: true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
dispatch(setInitialGasEstimate(selectedAggId))
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// A newer swap request is running, so simply bail and let the newer request respond
|
|
|
|
if (e.message === SWAPS_FETCH_ORDER_CONFLICT) {
|
|
|
|
log.debug(`Swap fetch order conflict detected; ignoring older request`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// TODO: Check for any errors we should expect to occur in production, and report others to Sentry
|
|
|
|
log.error(`Error fetching quotes: `, e)
|
|
|
|
|
|
|
|
dispatch(setSwapsErrorKey(ERROR_FETCHING_QUOTES))
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch(setFetchingQuotes(false))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const signAndSendTransactions = (history, metaMetricsEvent) => {
|
|
|
|
return async (dispatch, getState) => {
|
|
|
|
let swapsFeatureIsLive = false
|
|
|
|
try {
|
|
|
|
swapsFeatureIsLive = await fetchSwapsFeatureLiveness()
|
|
|
|
} catch (error) {
|
|
|
|
log.error('Failed to fetch Swaps liveness, defaulting to false.', error)
|
|
|
|
}
|
|
|
|
await dispatch(setSwapsLiveness(swapsFeatureIsLive))
|
|
|
|
|
|
|
|
if (!swapsFeatureIsLive) {
|
|
|
|
await history.push(SWAPS_MAINTENANCE_ROUTE)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const state = getState()
|
|
|
|
const customSwapsGas = getCustomSwapsGas(state)
|
|
|
|
const fetchParams = getFetchParams(state)
|
|
|
|
const { metaData, value: swapTokenValue, slippage } = fetchParams
|
|
|
|
const { sourceTokenInfo = {}, destinationTokenInfo = {} } = metaData
|
|
|
|
await dispatch(setBackgroundSwapRouteState('awaiting'))
|
|
|
|
await dispatch(stopPollingForQuotes())
|
|
|
|
history.push(AWAITING_SWAP_ROUTE)
|
|
|
|
|
|
|
|
const { fast: fastGasEstimate } = getSwapGasPriceEstimateData(state)
|
|
|
|
|
|
|
|
const usedQuote = getUsedQuote(state)
|
|
|
|
const usedTradeTxParams = usedQuote.trade
|
|
|
|
|
|
|
|
const estimatedGasLimit = new BigNumber(
|
|
|
|
usedQuote?.gasEstimate || decimalToHex(usedQuote?.averageGas || 0),
|
|
|
|
16,
|
|
|
|
)
|
|
|
|
const estimatedGasLimitWithMultiplier = estimatedGasLimit
|
|
|
|
.times(1.4, 10)
|
|
|
|
.round(0)
|
|
|
|
.toString(16)
|
|
|
|
const maxGasLimit =
|
|
|
|
customSwapsGas ||
|
|
|
|
hexMax(
|
|
|
|
`0x${decimalToHex(usedQuote?.maxGas || 0)}`,
|
|
|
|
estimatedGasLimitWithMultiplier,
|
|
|
|
)
|
|
|
|
|
|
|
|
const usedGasPrice = getUsedSwapsGasPrice(state)
|
|
|
|
usedTradeTxParams.gas = maxGasLimit
|
|
|
|
usedTradeTxParams.gasPrice = usedGasPrice
|
|
|
|
|
|
|
|
const usdConversionRate = getUSDConversionRate(state)
|
|
|
|
const destinationValue = calcTokenAmount(
|
|
|
|
usedQuote.destinationAmount,
|
|
|
|
destinationTokenInfo.decimals || 18,
|
|
|
|
).toPrecision(8)
|
|
|
|
const usedGasLimitEstimate =
|
|
|
|
usedQuote?.gasEstimateWithRefund ||
|
|
|
|
`0x${decimalToHex(usedQuote?.averageGas || 0)}`
|
|
|
|
const totalGasLimitEstimate = new BigNumber(usedGasLimitEstimate, 16)
|
|
|
|
.plus(usedQuote.approvalNeeded?.gas || '0x0', 16)
|
|
|
|
.toString(16)
|
|
|
|
const gasEstimateTotalInUSD = getValueFromWeiHex({
|
|
|
|
value: calcGasTotal(totalGasLimitEstimate, usedGasPrice),
|
|
|
|
toCurrency: 'usd',
|
|
|
|
conversionRate: usdConversionRate,
|
|
|
|
numberOfDecimals: 6,
|
|
|
|
})
|
|
|
|
|
|
|
|
const swapMetaData = {
|
|
|
|
token_from: sourceTokenInfo.symbol,
|
|
|
|
token_from_amount: String(swapTokenValue),
|
|
|
|
token_to: destinationTokenInfo.symbol,
|
|
|
|
token_to_amount: destinationValue,
|
|
|
|
slippage,
|
|
|
|
custom_slippage: slippage !== 2,
|
|
|
|
best_quote_source: getTopQuote(state)?.aggregator,
|
|
|
|
available_quotes: getQuotes(state)?.length,
|
|
|
|
other_quote_selected:
|
|
|
|
usedQuote.aggregator !== getTopQuote(state)?.aggregator,
|
|
|
|
other_quote_selected_source:
|
|
|
|
usedQuote.aggregator === getTopQuote(state)?.aggregator
|
|
|
|
? ''
|
|
|
|
: usedQuote.aggregator,
|
|
|
|
gas_fees: gasEstimateTotalInUSD,
|
|
|
|
estimated_gas: estimatedGasLimit.toString(10),
|
|
|
|
suggested_gas_price: fastGasEstimate,
|
|
|
|
used_gas_price: hexWEIToDecGWEI(usedGasPrice),
|
|
|
|
average_savings: usedQuote.savings?.total,
|
|
|
|
performance_savings: usedQuote.savings?.performance,
|
|
|
|
fee_savings: usedQuote.savings?.fee,
|
|
|
|
median_metamask_fee: usedQuote.savings?.medianMetaMaskFee,
|
|
|
|
}
|
|
|
|
|
|
|
|
const metaMetricsConfig = {
|
|
|
|
event: 'Swap Started',
|
|
|
|
category: 'swaps',
|
|
|
|
}
|
|
|
|
|
|
|
|
metaMetricsEvent({ ...metaMetricsConfig })
|
|
|
|
metaMetricsEvent({
|
|
|
|
...metaMetricsConfig,
|
|
|
|
excludeMetaMetricsId: true,
|
|
|
|
properties: swapMetaData,
|
|
|
|
})
|
|
|
|
|
|
|
|
let finalApproveTxMeta
|
|
|
|
const approveTxParams = getApproveTxParams(state)
|
|
|
|
if (approveTxParams) {
|
|
|
|
const approveTxMeta = await dispatch(
|
|
|
|
addUnapprovedTransaction(
|
|
|
|
{ ...approveTxParams, amount: '0x0' },
|
|
|
|
'metamask',
|
|
|
|
),
|
|
|
|
)
|
|
|
|
await dispatch(setApproveTxId(approveTxMeta.id))
|
|
|
|
finalApproveTxMeta = await dispatch(
|
|
|
|
updateTransaction(
|
|
|
|
{
|
|
|
|
...approveTxMeta,
|
|
|
|
transactionCategory: TRANSACTION_CATEGORIES.SWAP_APPROVAL,
|
|
|
|
sourceTokenSymbol: sourceTokenInfo.symbol,
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
await dispatch(updateAndApproveTx(finalApproveTxMeta, true))
|
|
|
|
} catch (e) {
|
|
|
|
await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR))
|
|
|
|
history.push(SWAPS_ERROR_ROUTE)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const tradeTxMeta = await dispatch(
|
|
|
|
addUnapprovedTransaction(usedTradeTxParams, 'metamask'),
|
|
|
|
)
|
|
|
|
dispatch(setTradeTxId(tradeTxMeta.id))
|
|
|
|
const finalTradeTxMeta = await dispatch(
|
|
|
|
updateTransaction(
|
|
|
|
{
|
|
|
|
...tradeTxMeta,
|
|
|
|
sourceTokenSymbol: sourceTokenInfo.symbol,
|
|
|
|
destinationTokenSymbol: destinationTokenInfo.symbol,
|
|
|
|
transactionCategory: TRANSACTION_CATEGORIES.SWAP,
|
|
|
|
destinationTokenDecimals: destinationTokenInfo.decimals,
|
|
|
|
destinationTokenAddress: destinationTokenInfo.address,
|
|
|
|
swapMetaData,
|
|
|
|
swapTokenValue,
|
|
|
|
approvalTxId: finalApproveTxMeta?.id,
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
await dispatch(updateAndApproveTx(finalTradeTxMeta, true))
|
|
|
|
} catch (e) {
|
|
|
|
await dispatch(setSwapsErrorKey(SWAP_FAILED_ERROR))
|
|
|
|
history.push(SWAPS_ERROR_ROUTE)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
await forceUpdateMetamaskState(dispatch)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function fetchMetaSwapsGasPriceEstimates() {
|
|
|
|
return async (dispatch, getState) => {
|
|
|
|
const state = getState()
|
|
|
|
const priceEstimatesLastRetrieved = getSwapsPriceEstimatesLastRetrieved(
|
|
|
|
state,
|
|
|
|
)
|
|
|
|
const timeLastRetrieved =
|
|
|
|
priceEstimatesLastRetrieved ||
|
|
|
|
loadLocalStorageData('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED') ||
|
|
|
|
0
|
|
|
|
|
|
|
|
dispatch(swapGasPriceEstimatesFetchStarted())
|
|
|
|
|
|
|
|
let priceEstimates
|
|
|
|
try {
|
|
|
|
if (Date.now() - timeLastRetrieved > 30000) {
|
|
|
|
priceEstimates = await fetchSwapsGasPrices()
|
|
|
|
} else {
|
|
|
|
const cachedPriceEstimates = loadLocalStorageData(
|
|
|
|
'METASWAP_GAS_PRICE_ESTIMATES',
|
|
|
|
)
|
|
|
|
priceEstimates = cachedPriceEstimates || (await fetchSwapsGasPrices())
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
log.warn('Fetching swaps gas prices failed:', e)
|
|
|
|
|
|
|
|
if (!e.message?.match(/NetworkError|Fetch failed with status:/u)) {
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch(swapGasPriceEstimatesFetchFailed())
|
|
|
|
|
|
|
|
try {
|
|
|
|
const gasPrice = await global.ethQuery.gasPrice()
|
|
|
|
const gasPriceInDecGWEI = hexWEIToDecGWEI(gasPrice.toString(10))
|
|
|
|
|
|
|
|
dispatch(retrievedFallbackSwapsGasPrice(gasPriceInDecGWEI))
|
|
|
|
return null
|
|
|
|
} catch (networkGasPriceError) {
|
|
|
|
console.error(
|
|
|
|
`Failed to retrieve fallback gas price: `,
|
|
|
|
networkGasPriceError,
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const timeRetrieved = Date.now()
|
|
|
|
|
|
|
|
saveLocalStorageData(priceEstimates, 'METASWAP_GAS_PRICE_ESTIMATES')
|
|
|
|
saveLocalStorageData(
|
|
|
|
timeRetrieved,
|
|
|
|
'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED',
|
|
|
|
)
|
|
|
|
|
|
|
|
dispatch(
|
|
|
|
swapGasPriceEstimatesFetchCompleted({
|
|
|
|
priceEstimates,
|
|
|
|
priceEstimatesLastRetrieved: timeRetrieved,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
return priceEstimates
|
|
|
|
}
|
|
|
|
}
|