@ -1,54 +1,57 @@
import { ethers } from 'ethers'
import log from 'loglevel'
import BigNumber from 'bignumber.js'
import { ObservableStore } from '@metamask/obs-store'
import { mapValues , cloneDeep } from 'lodash'
import abi from 'human-standard-token-abi'
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util'
import { ethers } from 'ethers' ;
import log from 'loglevel' ;
import BigNumber from 'bignumber.js' ;
import { ObservableStore } from '@metamask/obs-store' ;
import { mapValues , cloneDeep } from 'lodash' ;
import abi from 'human-standard-token-abi' ;
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util' ;
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils' ;
import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util' ;
import {
ETH _SWAPS _TOKEN _ADDRESS ,
DEFAULT _ERC20 _APPROVE _GAS ,
QUOTES _EXPIRED _ERROR ,
QUOTES _NOT _AVAILABLE _ERROR ,
SWAPS _FETCH _ORDER _CONFLICT ,
} from '../../../ui/app/helpers/constants/swaps'
} from '../../../ui/app/helpers/constants/swaps' ;
import {
fetchTradesInfo as defaultFetchTradesInfo ,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness ,
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime ,
} from '../../../ui/app/pages/swaps/swaps.util'
} from '../../../ui/app/pages/swaps/swaps.util' ;
const METASWAP _ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'
const METASWAP _ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c' ;
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
const MAX _GAS _LIMIT = 2500000
const MAX _GAS _LIMIT = 2500000 ;
// To ensure that our serves are not spammed if MetaMask is left idle, we limit the number of fetches for quotes that are made on timed intervals.
// 3 seems to be an appropriate balance of giving users the time they need when MetaMask is not left idle, and turning polling off when it is.
const POLL _COUNT _LIMIT = 3
const POLL _COUNT _LIMIT = 3 ;
// If for any reason the MetaSwap API fails to provide a refresh time,
// provide a reasonable fallback to avoid further errors
const FALLBACK _QUOTE _REFRESH _TIME = 60000
const FALLBACK _QUOTE _REFRESH _TIME = 60000 ;
// This is the amount of time to wait, after successfully fetching quotes
// and their gas estimates, before fetching for new quotes
const QUOTE _POLLING _DIFFERENCE _INTERVAL = 10 * 1000
const QUOTE _POLLING _DIFFERENCE _INTERVAL = 10 * 1000 ;
function calculateGasEstimateWithRefund (
maxGas = MAX _GAS _LIMIT ,
estimatedRefund = 0 ,
estimatedGas = 0 ,
) {
const maxGasMinusRefund = new BigNumber ( maxGas , 10 ) . minus ( estimatedRefund , 10 )
const maxGasMinusRefund = new BigNumber ( maxGas , 10 ) . minus (
estimatedRefund ,
10 ,
) ;
const gasEstimateWithRefund = maxGasMinusRefund . lt ( estimatedGas , 16 )
? maxGasMinusRefund . toString ( 16 )
: estimatedGas
: estimatedGas ;
return gasEstimateWithRefund
return gasEstimateWithRefund ;
}
const initialState = {
@ -69,7 +72,7 @@ const initialState = {
swapsFeatureIsLive : false ,
swapsQuoteRefreshTime : FALLBACK _QUOTE _REFRESH _TIME ,
} ,
}
} ;
export default class SwapsController {
constructor ( {
@ -84,46 +87,46 @@ export default class SwapsController {
} ) {
this . store = new ObservableStore ( {
swapsState : { ... initialState . swapsState } ,
} )
} ) ;
this . _fetchTradesInfo = fetchTradesInfo
this . _fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness
this . _fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime
this . _fetchTradesInfo = fetchTradesInfo ;
this . _fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness ;
this . _fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime ;
this . getBufferedGasLimit = getBufferedGasLimit
this . tokenRatesStore = tokenRatesStore
this . getBufferedGasLimit = getBufferedGasLimit ;
this . tokenRatesStore = tokenRatesStore ;
this . pollCount = 0
this . getProviderConfig = getProviderConfig
this . pollCount = 0 ;
this . getProviderConfig = getProviderConfig ;
this . indexOfNewestCallInFlight = 0
this . indexOfNewestCallInFlight = 0 ;
this . ethersProvider = new ethers . providers . Web3Provider ( provider )
this . _currentNetwork = networkController . store . getState ( ) . network
this . ethersProvider = new ethers . providers . Web3Provider ( provider ) ;
this . _currentNetwork = networkController . store . getState ( ) . network ;
networkController . on ( 'networkDidChange' , ( network ) => {
if ( network !== 'loading' && network !== this . _currentNetwork ) {
this . _currentNetwork = network
this . ethersProvider = new ethers . providers . Web3Provider ( provider )
this . _currentNetwork = network ;
this . ethersProvider = new ethers . providers . Web3Provider ( provider ) ;
}
} )
} ) ;
this . _setupSwapsLivenessFetching ( )
this . _setupSwapsLivenessFetching ( ) ;
}
// Sets the refresh rate for quote updates from the MetaSwap API
async _setSwapsQuoteRefreshTime ( ) {
// Default to fallback time unless API returns valid response
let swapsQuoteRefreshTime = FALLBACK _QUOTE _REFRESH _TIME
let swapsQuoteRefreshTime = FALLBACK _QUOTE _REFRESH _TIME ;
try {
swapsQuoteRefreshTime = await this . _fetchSwapsQuoteRefreshTime ( )
swapsQuoteRefreshTime = await this . _fetchSwapsQuoteRefreshTime ( ) ;
} catch ( e ) {
console . error ( 'Request for swaps quote refresh time failed: ' , e )
console . error ( 'Request for swaps quote refresh time failed: ' , e ) ;
}
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , swapsQuoteRefreshTime } ,
} )
} ) ;
}
// Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough
@ -133,20 +136,20 @@ export default class SwapsController {
pollForNewQuotes ( ) {
const {
swapsState : { swapsQuoteRefreshTime } ,
} = this . store . getState ( )
} = this . store . getState ( ) ;
this . pollingTimeout = setTimeout ( ( ) => {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . fetchAndSetQuotes (
swapsState . fetchParams ,
swapsState . fetchParams ? . metaData ,
true ,
)
} , swapsQuoteRefreshTime - QUOTE _POLLING _DIFFERENCE _INTERVAL )
) ;
} , swapsQuoteRefreshTime - QUOTE _POLLING _DIFFERENCE _INTERVAL ) ;
}
stopPollingForQuotes ( ) {
clearTimeout ( this . pollingTimeout )
clearTimeout ( this . pollingTimeout ) ;
}
async fetchAndSetQuotes (
@ -155,37 +158,37 @@ export default class SwapsController {
isPolledRequest ,
) {
if ( ! fetchParams ) {
return null
return null ;
}
// Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params.
if ( ! isPolledRequest ) {
this . pollCount = 0
this . pollCount = 0 ;
}
// If there are any pending poll requests, clear them so that they don't get call while this new fetch is in process
clearTimeout ( this . pollingTimeout )
clearTimeout ( this . pollingTimeout ) ;
if ( ! isPolledRequest ) {
this . setSwapsErrorKey ( '' )
this . setSwapsErrorKey ( '' ) ;
}
const indexOfCurrentCall = this . indexOfNewestCallInFlight + 1
this . indexOfNewestCallInFlight = indexOfCurrentCall
const indexOfCurrentCall = this . indexOfNewestCallInFlight + 1 ;
this . indexOfNewestCallInFlight = indexOfCurrentCall ;
let [ newQuotes ] = await Promise . all ( [
this . _fetchTradesInfo ( fetchParams ) ,
this . _setSwapsQuoteRefreshTime ( ) ,
] )
] ) ;
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
sourceTokenInfo : fetchParamsMetaData . sourceTokenInfo ,
destinationTokenInfo : fetchParamsMetaData . destinationTokenInfo ,
} ) )
} ) ) ;
const quotesLastFetched = Date . now ( )
const quotesLastFetched = Date . now ( ) ;
let approvalRequired = false
let approvalRequired = false ;
if (
fetchParams . sourceToken !== ETH _SWAPS _TOKEN _ADDRESS &&
Object . values ( newQuotes ) . length
@ -193,22 +196,22 @@ export default class SwapsController {
const allowance = await this . _getERC20Allowance (
fetchParams . sourceToken ,
fetchParams . fromAddress ,
)
) ;
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
// than 0, it means that approval has already occured and is not needed. Otherwise, for tokens to be swapped, a new
// call of the ERC-20 approve method is required.
approvalRequired = allowance . eq ( 0 )
approvalRequired = allowance . eq ( 0 ) ;
if ( ! approvalRequired ) {
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
approvalNeeded : null ,
} ) )
} ) ) ;
} else if ( ! isPolledRequest ) {
const { gasLimit : approvalGas } = await this . timedoutGasReturn (
Object . values ( newQuotes ) [ 0 ] . approvalNeeded ,
)
) ;
newQuotes = mapValues ( newQuotes , ( quote ) => ( {
... quote ,
@ -216,39 +219,39 @@ export default class SwapsController {
... quote . approvalNeeded ,
gas : approvalGas || DEFAULT _ERC20 _APPROVE _GAS ,
} ,
} ) )
} ) ) ;
}
}
let topAggId = null
let topAggId = null ;
// We can reduce time on the loading screen by only doing this after the
// loading screen and best quote have rendered.
if ( ! approvalRequired && ! fetchParams ? . balanceError ) {
newQuotes = await this . getAllQuotesWithGasEstimates ( newQuotes )
newQuotes = await this . getAllQuotesWithGasEstimates ( newQuotes ) ;
}
if ( Object . values ( newQuotes ) . length === 0 ) {
this . setSwapsErrorKey ( QUOTES _NOT _AVAILABLE _ERROR )
this . setSwapsErrorKey ( QUOTES _NOT _AVAILABLE _ERROR ) ;
} else {
const [
_topAggId ,
quotesWithSavingsAndFeeData ,
] = await this . _findTopQuoteAndCalculateSavings ( newQuotes )
topAggId = _topAggId
newQuotes = quotesWithSavingsAndFeeData
] = await this . _findTopQuoteAndCalculateSavings ( newQuotes ) ;
topAggId = _topAggId ;
newQuotes = quotesWithSavingsAndFeeData ;
}
// If a newer call has been made, don't update state with old information
// Prevents timing conflicts between fetches
if ( this . indexOfNewestCallInFlight !== indexOfCurrentCall ) {
throw new Error ( SWAPS _FETCH _ORDER _CONFLICT )
throw new Error ( SWAPS _FETCH _ORDER _CONFLICT ) ;
}
const { swapsState } = this . store . getState ( )
let { selectedAggId } = swapsState
const { swapsState } = this . store . getState ( ) ;
let { selectedAggId } = swapsState ;
if ( ! newQuotes [ selectedAggId ] ) {
selectedAggId = null
selectedAggId = null ;
}
this . store . updateState ( {
@ -260,41 +263,41 @@ export default class SwapsController {
selectedAggId ,
topAggId ,
} ,
} )
} ) ;
// We only want to do up to a maximum of three requests from polling.
this . pollCount += 1
this . pollCount += 1 ;
if ( this . pollCount < POLL _COUNT _LIMIT + 1 ) {
this . pollForNewQuotes ( )
this . pollForNewQuotes ( ) ;
} else {
this . resetPostFetchState ( )
this . setSwapsErrorKey ( QUOTES _EXPIRED _ERROR )
return null
this . resetPostFetchState ( ) ;
this . setSwapsErrorKey ( QUOTES _EXPIRED _ERROR ) ;
return null ;
}
return [ newQuotes , topAggId ]
return [ newQuotes , topAggId ] ;
}
safeRefetchQuotes ( ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
if ( ! this . pollingTimeout && swapsState . fetchParams ) {
this . fetchAndSetQuotes ( swapsState . fetchParams )
this . fetchAndSetQuotes ( swapsState . fetchParams ) ;
}
}
setSelectedQuoteAggId ( selectedAggId ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , selectedAggId } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , selectedAggId } } ) ;
}
setSwapsTokens ( tokens ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , tokens } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , tokens } } ) ;
}
setSwapsErrorKey ( errorKey ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , errorKey } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , errorKey } } ) ;
}
async getAllQuotesWithGasEstimates ( quotes ) {
@ -302,43 +305,43 @@ export default class SwapsController {
Object . values ( quotes ) . map ( async ( quote ) => {
const { gasLimit , simulationFails } = await this . timedoutGasReturn (
quote . trade ,
)
return [ gasLimit , simulationFails , quote . aggregator ]
) ;
return [ gasLimit , simulationFails , quote . aggregator ] ;
} ) ,
)
) ;
const newQuotes = { }
const newQuotes = { } ;
quoteGasData . forEach ( ( [ gasLimit , simulationFails , aggId ] ) => {
if ( gasLimit && ! simulationFails ) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund (
quotes [ aggId ] . maxGas ,
quotes [ aggId ] . estimatedRefund ,
gasLimit ,
)
) ;
newQuotes [ aggId ] = {
... quotes [ aggId ] ,
gasEstimate : gasLimit ,
gasEstimateWithRefund ,
}
} ;
} else if ( quotes [ aggId ] . approvalNeeded ) {
// If gas estimation fails, but an ERC-20 approve is needed, then we do not add any estimate property to the quote object
// Such quotes will rely on the maxGas and averageGas properties from the api
newQuotes [ aggId ] = quotes [ aggId ]
newQuotes [ aggId ] = quotes [ aggId ] ;
}
// If gas estimation fails and no approval is needed, then we filter that quote out, so that it is not shown to the user
} )
return newQuotes
} ) ;
return newQuotes ;
}
timedoutGasReturn ( tradeTxParams ) {
return new Promise ( ( resolve ) => {
let gasTimedOut = false
let gasTimedOut = false ;
const gasTimeout = setTimeout ( ( ) => {
gasTimedOut = true
resolve ( { gasLimit : null , simulationFails : true } )
} , 5000 )
gasTimedOut = true ;
resolve ( { gasLimit : null , simulationFails : true } ) ;
} , 5000 ) ;
// Remove gas from params that will be passed to the `estimateGas` call
// Including it can cause the estimate to fail if the actual gas needed
@ -348,44 +351,44 @@ export default class SwapsController {
from : tradeTxParams . from ,
to : tradeTxParams . to ,
value : tradeTxParams . value ,
}
} ;
this . getBufferedGasLimit ( { txParams : tradeTxParamsForGasEstimate } , 1 )
. then ( ( { gasLimit , simulationFails } ) => {
if ( ! gasTimedOut ) {
clearTimeout ( gasTimeout )
resolve ( { gasLimit , simulationFails } )
clearTimeout ( gasTimeout ) ;
resolve ( { gasLimit , simulationFails } ) ;
}
} )
. catch ( ( e ) => {
log . error ( e )
log . error ( e ) ;
if ( ! gasTimedOut ) {
clearTimeout ( gasTimeout )
resolve ( { gasLimit : null , simulationFails : true } )
clearTimeout ( gasTimeout ) ;
resolve ( { gasLimit : null , simulationFails : true } ) ;
}
} )
} )
} ) ;
} ) ;
}
async setInitialGasEstimate ( initialAggId ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
const quoteToUpdate = { ... swapsState . quotes [ initialAggId ] }
const quoteToUpdate = { ... swapsState . quotes [ initialAggId ] } ;
const {
gasLimit : newGasEstimate ,
simulationFails ,
} = await this . timedoutGasReturn ( quoteToUpdate . trade )
} = await this . timedoutGasReturn ( quoteToUpdate . trade ) ;
if ( newGasEstimate && ! simulationFails ) {
const gasEstimateWithRefund = calculateGasEstimateWithRefund (
quoteToUpdate . maxGas ,
quoteToUpdate . estimatedRefund ,
newGasEstimate ,
)
) ;
quoteToUpdate . gasEstimate = newGasEstimate
quoteToUpdate . gasEstimateWithRefund = gasEstimateWithRefund
quoteToUpdate . gasEstimate = newGasEstimate ;
quoteToUpdate . gasEstimateWithRefund = gasEstimateWithRefund ;
}
this . store . updateState ( {
@ -393,59 +396,61 @@ export default class SwapsController {
... swapsState ,
quotes : { ... swapsState . quotes , [ initialAggId ] : quoteToUpdate } ,
} ,
} )
} ) ;
}
setApproveTxId ( approveTxId ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , approveTxId } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , approveTxId } } ) ;
}
setTradeTxId ( tradeTxId ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , tradeTxId } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , tradeTxId } } ) ;
}
setQuotesLastFetched ( quotesLastFetched ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , quotesLastFetched } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , quotesLastFetched } ,
} ) ;
}
setSwapsTxGasPrice ( gasPrice ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , customGasPrice : gasPrice } ,
} )
} ) ;
}
setSwapsTxGasLimit ( gasLimit ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , customMaxGas : gasLimit } ,
} )
} ) ;
}
setCustomApproveTxData ( data ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , customApproveTxData : data } ,
} )
} ) ;
}
setBackgroundSwapRouteState ( routeState ) {
const { swapsState } = this . store . getState ( )
this . store . updateState ( { swapsState : { ... swapsState , routeState } } )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( { swapsState : { ... swapsState , routeState } } ) ;
}
setSwapsLiveness ( swapsFeatureIsLive ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : { ... swapsState , swapsFeatureIsLive } ,
} )
} ) ;
}
resetPostFetchState ( ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : {
@ -455,12 +460,12 @@ export default class SwapsController {
swapsFeatureIsLive : swapsState . swapsFeatureIsLive ,
swapsQuoteRefreshTime : swapsState . swapsQuoteRefreshTime ,
} ,
} )
clearTimeout ( this . pollingTimeout )
} ) ;
clearTimeout ( this . pollingTimeout ) ;
}
resetSwapsState ( ) {
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
this . store . updateState ( {
swapsState : {
@ -469,33 +474,33 @@ export default class SwapsController {
swapsFeatureIsLive : swapsState . swapsFeatureIsLive ,
swapsQuoteRefreshTime : swapsState . swapsQuoteRefreshTime ,
} ,
} )
clearTimeout ( this . pollingTimeout )
} ) ;
clearTimeout ( this . pollingTimeout ) ;
}
async _getEthersGasPrice ( ) {
const ethersGasPrice = await this . ethersProvider . getGasPrice ( )
return ethersGasPrice . toHexString ( )
const ethersGasPrice = await this . ethersProvider . getGasPrice ( ) ;
return ethersGasPrice . toHexString ( ) ;
}
async _findTopQuoteAndCalculateSavings ( quotes = { } ) {
const tokenConversionRates = this . tokenRatesStore . getState ( )
. contractExchangeRates
. contractExchangeRates ;
const {
swapsState : { customGasPrice } ,
} = this . store . getState ( )
} = this . store . getState ( ) ;
const numQuotes = Object . keys ( quotes ) . length
const numQuotes = Object . keys ( quotes ) . length ;
if ( ! numQuotes ) {
return { }
return { } ;
}
const newQuotes = cloneDeep ( quotes )
const newQuotes = cloneDeep ( quotes ) ;
const usedGasPrice = customGasPrice || ( await this . _getEthersGasPrice ( ) )
const usedGasPrice = customGasPrice || ( await this . _getEthersGasPrice ( ) ) ;
let topAggId = null
let overallValueOfBestQuoteForSorting = null
let topAggId = null ;
let overallValueOfBestQuoteForSorting = null ;
Object . values ( newQuotes ) . forEach ( ( quote ) => {
const {
@ -510,20 +515,20 @@ export default class SwapsController {
sourceToken ,
trade ,
fee : metaMaskFee ,
} = quote
} = quote ;
const tradeGasLimitForCalculation = gasEstimate
? new BigNumber ( gasEstimate , 16 )
: new BigNumber ( averageGas || MAX _GAS _LIMIT , 10 )
: new BigNumber ( averageGas || MAX _GAS _LIMIT , 10 ) ;
const totalGasLimitForCalculation = tradeGasLimitForCalculation
. plus ( approvalNeeded ? . gas || '0x0' , 16 )
. toString ( 16 )
. toString ( 16 ) ;
const gasTotalInWeiHex = calcGasTotal (
totalGasLimitForCalculation ,
usedGasPrice ,
)
) ;
// trade.value is a sum of different values depending on the transaction.
// It always includes any external fees charged by the quote source. In
@ -532,7 +537,7 @@ export default class SwapsController {
const totalWeiCost = new BigNumber ( gasTotalInWeiHex , 16 ) . plus (
trade . value ,
16 ,
)
) ;
const totalEthCost = conversionUtil ( totalWeiCost , {
fromCurrency : 'ETH' ,
@ -540,7 +545,7 @@ export default class SwapsController {
toDenomination : 'ETH' ,
fromNumericBase : 'BN' ,
numberOfDecimals : 6 ,
} )
} ) ;
// The total fee is aggregator/exchange fees plus gas fees.
// If the swap is from ETH, subtract the sourceAmount from the total cost.
@ -557,103 +562,103 @@ export default class SwapsController {
numberOfDecimals : 6 ,
} ,
)
: totalEthCost
: totalEthCost ;
const decimalAdjustedDestinationAmount = calcTokenAmount (
destinationAmount ,
destinationTokenInfo . decimals ,
)
) ;
const tokenPercentageOfPreFeeDestAmount = new BigNumber ( 100 , 10 )
. minus ( metaMaskFee , 10 )
. div ( 100 )
. div ( 100 ) ;
const destinationAmountBeforeMetaMaskFee = decimalAdjustedDestinationAmount . div (
tokenPercentageOfPreFeeDestAmount ,
)
) ;
const metaMaskFeeInTokens = destinationAmountBeforeMetaMaskFee . minus (
decimalAdjustedDestinationAmount ,
)
) ;
const tokenConversionRate = tokenConversionRates [ destinationToken ]
const conversionRateForSorting = tokenConversionRate || 1
const tokenConversionRate = tokenConversionRates [ destinationToken ] ;
const conversionRateForSorting = tokenConversionRate || 1 ;
const ethValueOfTokens = decimalAdjustedDestinationAmount . times (
conversionRateForSorting ,
10 ,
)
) ;
const conversionRateForCalculations =
destinationToken === ETH _SWAPS _TOKEN _ADDRESS ? 1 : tokenConversionRate
destinationToken === ETH _SWAPS _TOKEN _ADDRESS ? 1 : tokenConversionRate ;
const overallValueOfQuoteForSorting =
conversionRateForCalculations === undefined
? ethValueOfTokens
: ethValueOfTokens . minus ( ethFee , 10 )
: ethValueOfTokens . minus ( ethFee , 10 ) ;
quote . ethFee = ethFee . toString ( 10 )
quote . ethFee = ethFee . toString ( 10 ) ;
if ( conversionRateForCalculations !== undefined ) {
quote . ethValueOfTokens = ethValueOfTokens . toString ( 10 )
quote . overallValueOfQuote = overallValueOfQuoteForSorting . toString ( 10 )
quote . ethValueOfTokens = ethValueOfTokens . toString ( 10 ) ;
quote . overallValueOfQuote = overallValueOfQuoteForSorting . toString ( 10 ) ;
quote . metaMaskFeeInEth = metaMaskFeeInTokens
. times ( conversionRateForCalculations )
. toString ( 10 )
. toString ( 10 ) ;
}
if (
overallValueOfBestQuoteForSorting === null ||
overallValueOfQuoteForSorting . gt ( overallValueOfBestQuoteForSorting )
) {
topAggId = aggregator
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting
topAggId = aggregator ;
overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting ;
}
} )
} ) ;
const isBest =
newQuotes [ topAggId ] . destinationToken === ETH _SWAPS _TOKEN _ADDRESS ||
Boolean ( tokenConversionRates [ newQuotes [ topAggId ] ? . destinationToken ] )
Boolean ( tokenConversionRates [ newQuotes [ topAggId ] ? . destinationToken ] ) ;
let savings = null
let savings = null ;
if ( isBest ) {
const bestQuote = newQuotes [ topAggId ]
const bestQuote = newQuotes [ topAggId ] ;
savings = { }
savings = { } ;
const {
ethFee : medianEthFee ,
metaMaskFeeInEth : medianMetaMaskFee ,
ethValueOfTokens : medianEthValueOfTokens ,
} = getMedianEthValueQuote ( Object . values ( newQuotes ) )
} = getMedianEthValueQuote ( Object . values ( newQuotes ) ) ;
// Performance savings are calculated as:
// (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings . performance = new BigNumber ( bestQuote . ethValueOfTokens , 10 ) . minus (
medianEthValueOfTokens ,
10 ,
)
) ;
// Fee savings are calculated as:
// (fee for the median trade) - (fee for the best trade)
savings . fee = new BigNumber ( medianEthFee ) . minus ( bestQuote . ethFee , 10 )
savings . fee = new BigNumber ( medianEthFee ) . minus ( bestQuote . ethFee , 10 ) ;
savings . metaMaskFee = bestQuote . metaMaskFeeInEth
savings . metaMaskFee = bestQuote . metaMaskFeeInEth ;
// Total savings are calculated as:
// performance savings + fee savings - metamask fee
savings . total = savings . performance
. plus ( savings . fee )
. minus ( savings . metaMaskFee )
. toString ( 10 )
savings . performance = savings . performance . toString ( 10 )
savings . fee = savings . fee . toString ( 10 )
savings . medianMetaMaskFee = medianMetaMaskFee
. toString ( 10 ) ;
savings . performance = savings . performance . toString ( 10 ) ;
savings . fee = savings . fee . toString ( 10 ) ;
savings . medianMetaMaskFee = medianMetaMaskFee ;
newQuotes [ topAggId ] . isBestQuote = true
newQuotes [ topAggId ] . savings = savings
newQuotes [ topAggId ] . isBestQuote = true ;
newQuotes [ topAggId ] . savings = savings ;
}
return [ topAggId , newQuotes ]
return [ topAggId , newQuotes ] ;
}
async _getERC20Allowance ( contractAddress , walletAddress ) {
@ -661,8 +666,8 @@ export default class SwapsController {
contractAddress ,
abi ,
this . ethersProvider ,
)
return await contract . allowance ( walletAddress , METASWAP _ADDRESS )
) ;
return await contract . allowance ( walletAddress , METASWAP _ADDRESS ) ;
}
/ * *
@ -674,8 +679,8 @@ export default class SwapsController {
* until the value can be fetched again .
* /
_setupSwapsLivenessFetching ( ) {
const TEN _MINUTES _MS = 10 * 60 * 1000
let intervalId = null
const TEN _MINUTES _MS = 10 * 60 * 1000 ;
let intervalId = null ;
const fetchAndSetupInterval = ( ) => {
if ( window . navigator . onLine && intervalId === null ) {
@ -684,25 +689,25 @@ export default class SwapsController {
intervalId = setInterval (
this . _fetchAndSetSwapsLiveness . bind ( this ) ,
TEN _MINUTES _MS ,
)
this . _fetchAndSetSwapsLiveness ( )
}
) ;
this . _fetchAndSetSwapsLiveness ( ) ;
}
} ;
window . addEventListener ( 'online' , fetchAndSetupInterval )
window . addEventListener ( 'online' , fetchAndSetupInterval ) ;
window . addEventListener ( 'offline' , ( ) => {
if ( intervalId !== null ) {
clearInterval ( intervalId )
intervalId = null
clearInterval ( intervalId ) ;
intervalId = null ;
const { swapsState } = this . store . getState ( )
const { swapsState } = this . store . getState ( ) ;
if ( swapsState . swapsFeatureIsLive ) {
this . setSwapsLiveness ( false )
this . setSwapsLiveness ( false ) ;
}
}
} )
} ) ;
fetchAndSetupInterval ( )
fetchAndSetupInterval ( ) ;
}
/ * *
@ -716,41 +721,41 @@ export default class SwapsController {
* state .
* /
async _fetchAndSetSwapsLiveness ( ) {
const { swapsState } = this . store . getState ( )
const { swapsFeatureIsLive : oldSwapsFeatureIsLive } = swapsState
let swapsFeatureIsLive = false
let successfullyFetched = false
let numAttempts = 0
const { swapsState } = this . store . getState ( ) ;
const { swapsFeatureIsLive : oldSwapsFeatureIsLive } = swapsState ;
let swapsFeatureIsLive = false ;
let successfullyFetched = false ;
let numAttempts = 0 ;
const fetchAndIncrementNumAttempts = async ( ) => {
try {
swapsFeatureIsLive = Boolean ( await this . _fetchSwapsFeatureLiveness ( ) )
successfullyFetched = true
swapsFeatureIsLive = Boolean ( await this . _fetchSwapsFeatureLiveness ( ) ) ;
successfullyFetched = true ;
} catch ( err ) {
log . error ( err )
numAttempts += 1
}
log . error ( err ) ;
numAttempts += 1 ;
}
} ;
await fetchAndIncrementNumAttempts ( )
await fetchAndIncrementNumAttempts ( ) ;
// The loop conditions are modified by fetchAndIncrementNumAttempts.
// eslint-disable-next-line no-unmodified-loop-condition
while ( ! successfullyFetched && numAttempts < 3 ) {
await new Promise ( ( resolve ) => {
setTimeout ( resolve , 5000 ) // 5 seconds
} )
await fetchAndIncrementNumAttempts ( )
setTimeout ( resolve , 5000 ) ; // 5 seconds
} ) ;
await fetchAndIncrementNumAttempts ( ) ;
}
if ( ! successfullyFetched ) {
log . error (
'Failed to fetch swaps feature flag 3 times. Setting to false and trying again next interval.' ,
)
) ;
}
if ( swapsFeatureIsLive !== oldSwapsFeatureIsLive ) {
this . setSwapsLiveness ( swapsFeatureIsLive )
this . setSwapsLiveness ( swapsFeatureIsLive ) ;
}
}
}
@ -763,50 +768,50 @@ export default class SwapsController {
* /
function getMedianEthValueQuote ( _quotes ) {
if ( ! Array . isArray ( _quotes ) || _quotes . length === 0 ) {
throw new Error ( 'Expected non-empty array param.' )
throw new Error ( 'Expected non-empty array param.' ) ;
}
const quotes = [ ... _quotes ]
const quotes = [ ... _quotes ] ;
quotes . sort ( ( quoteA , quoteB ) => {
const overallValueOfQuoteA = new BigNumber ( quoteA . overallValueOfQuote , 10 )
const overallValueOfQuoteB = new BigNumber ( quoteB . overallValueOfQuote , 10 )
const overallValueOfQuoteA = new BigNumber ( quoteA . overallValueOfQuote , 10 ) ;
const overallValueOfQuoteB = new BigNumber ( quoteB . overallValueOfQuote , 10 ) ;
if ( overallValueOfQuoteA . equals ( overallValueOfQuoteB ) ) {
return 0
return 0 ;
}
return overallValueOfQuoteA . lessThan ( overallValueOfQuoteB ) ? - 1 : 1
} )
return overallValueOfQuoteA . lessThan ( overallValueOfQuoteB ) ? - 1 : 1 ;
} ) ;
if ( quotes . length % 2 === 1 ) {
// return middle values
const medianOverallValue =
quotes [ ( quotes . length - 1 ) / 2 ] . overallValueOfQuote
quotes [ ( quotes . length - 1 ) / 2 ] . overallValueOfQuote ;
const quotesMatchingMedianQuoteValue = quotes . filter (
( quote ) => medianOverallValue === quote . overallValueOfQuote ,
)
return meansOfQuotesFeesAndValue ( quotesMatchingMedianQuoteValue )
) ;
return meansOfQuotesFeesAndValue ( quotesMatchingMedianQuoteValue ) ;
}
// return mean of middle two values
const upperIndex = quotes . length / 2
const lowerIndex = upperIndex - 1
const upperIndex = quotes . length / 2 ;
const lowerIndex = upperIndex - 1 ;
const overallValueAtUpperIndex = quotes [ upperIndex ] . overallValueOfQuote
const overallValueAtLowerIndex = quotes [ lowerIndex ] . overallValueOfQuote
const overallValueAtUpperIndex = quotes [ upperIndex ] . overallValueOfQuote ;
const overallValueAtLowerIndex = quotes [ lowerIndex ] . overallValueOfQuote ;
const quotesMatchingUpperIndexValue = quotes . filter (
( quote ) => overallValueAtUpperIndex === quote . overallValueOfQuote ,
)
) ;
const quotesMatchingLowerIndexValue = quotes . filter (
( quote ) => overallValueAtLowerIndex === quote . overallValueOfQuote ,
)
) ;
const feesAndValueAtUpperIndex = meansOfQuotesFeesAndValue (
quotesMatchingUpperIndexValue ,
)
) ;
const feesAndValueAtLowerIndex = meansOfQuotesFeesAndValue (
quotesMatchingLowerIndexValue ,
)
) ;
return {
ethFee : new BigNumber ( feesAndValueAtUpperIndex . ethFee , 10 )
@ -827,7 +832,7 @@ function getMedianEthValueQuote(_quotes) {
. plus ( feesAndValueAtLowerIndex . ethValueOfTokens , 10 )
. dividedBy ( 2 )
. toString ( 10 ) ,
}
} ;
}
/ * *
@ -857,7 +862,7 @@ function meansOfQuotesFeesAndValue(quotes) {
metaMaskFeeInEth : new BigNumber ( 0 , 10 ) ,
ethValueOfTokens : new BigNumber ( 0 , 10 ) ,
} ,
)
) ;
return {
ethFee : feeAndValueSumsAsBigNumbers . ethFee
@ -869,10 +874,10 @@ function meansOfQuotesFeesAndValue(quotes) {
ethValueOfTokens : feeAndValueSumsAsBigNumbers . ethValueOfTokens
. div ( quotes . length , 10 )
. toString ( 10 ) ,
}
} ;
}
export const utils = {
getMedianEthValueQuote ,
meansOfQuotesFeesAndValue ,
}
} ;