Alternative savings fix (#9675)

* Alternative savings fix

* Further required changes to savings fix

* Further fix to savings calculations that properly accounts for metamask fees

* metaMaskFeeInEth property on quotes to decimal string

* Fix swaps controller unit tests

* Improve documentation in swaps controller

* Prevent getMedianEthValueQuote from mutation passed quotes array with .sort() call

* Another fix and refactor to savings calculations in _findTopQuoteAndCalculateSavings

Cleaner structuring of conditionals for setting tokenValueOfQuoteForSorting, ethValueOfQuote and metaMaskFeeInEth in swaps controller

Stop subtracting medianMetaMaskFee from savings, but include it in savings data

Another fix and refactor to savings calculations in _findTopQuoteAndCalculateSavings

* Add and update unit tests for _findTopQuoteAndCalculateSavings

* Improve calculation of overallValueOfQuoteForSorting for case where ETH is the source token

* Clean up getMedianEthValueQuote code, test and comments

* Clean up _findTopQuoteAndCalculateSavings, create test input and expected results helper functions

* Update getMedianEthValueQuote to account for multiple quotes with overall values equal to the median

* Add jsdoc comment for meansOfQuotesFeesAndValue

* Fix jsdoc comment for getMedianEthValueQuote
feature/default_network_editable
Dan J Miller 4 years ago committed by GitHub
parent c4fad4b87f
commit c044b6f2b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 251
      app/scripts/controllers/swaps.js
  2. 652
      test/unit/app/controllers/swaps-test.js

@ -2,7 +2,7 @@ import { ethers } from 'ethers'
import log from 'loglevel' import log from 'loglevel'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
import { mapValues } from 'lodash' import { mapValues, cloneDeep } from 'lodash'
import abi from 'human-standard-token-abi' import abi from 'human-standard-token-abi'
import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util' import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'
import { calcGasTotal } from '../../../ui/app/pages/send/send.utils' import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'
@ -200,15 +200,12 @@ export default class SwapsController {
if (Object.values(newQuotes).length === 0) { if (Object.values(newQuotes).length === 0) {
this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR) this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)
} else { } else {
const topQuoteData = await this._findTopQuoteAndCalculateSavings( const [
newQuotes, _topAggId,
) quotesWithSavingsAndFeeData,
] = await this._findTopQuoteAndCalculateSavings(newQuotes)
if (topQuoteData.topAggId) { topAggId = _topAggId
topAggId = topQuoteData.topAggId newQuotes = quotesWithSavingsAndFeeData
newQuotes[topAggId].isBestQuote = topQuoteData.isBest
newQuotes[topAggId].savings = topQuoteData.savings
}
} }
// If a newer call has been made, don't update state with old information // If a newer call has been made, don't update state with old information
@ -460,15 +457,14 @@ export default class SwapsController {
return {} return {}
} }
const newQuotes = cloneDeep(quotes)
const usedGasPrice = customGasPrice || (await this._getEthersGasPrice()) const usedGasPrice = customGasPrice || (await this._getEthersGasPrice())
let topAggId = '' let topAggId = null
let ethTradeValueOfBestQuote = null let overallValueOfBestQuoteForSorting = null
let ethFeeForBestQuote = null
const allEthTradeValues = []
const allEthFees = []
Object.values(quotes).forEach((quote) => { Object.values(newQuotes).forEach((quote) => {
const { const {
aggregator, aggregator,
approvalNeeded, approvalNeeded,
@ -480,6 +476,7 @@ export default class SwapsController {
sourceAmount, sourceAmount,
sourceToken, sourceToken,
trade, trade,
fee: metaMaskFee,
} = quote } = quote
const tradeGasLimitForCalculation = gasEstimate const tradeGasLimitForCalculation = gasEstimate
@ -529,60 +526,101 @@ export default class SwapsController {
) )
: totalEthCost : totalEthCost
const decimalAdjustedDestinationAmount = calcTokenAmount(
destinationAmount,
destinationTokenInfo.decimals,
)
const tokenPercentageOfPreFeeDestAmount = new BigNumber(100, 10)
.minus(metaMaskFee, 10)
.div(100)
const destinationAmountBeforeMetaMaskFee = decimalAdjustedDestinationAmount.div(
tokenPercentageOfPreFeeDestAmount,
)
const metaMaskFeeInTokens = destinationAmountBeforeMetaMaskFee.minus(
decimalAdjustedDestinationAmount,
)
const tokenConversionRate = tokenConversionRates[destinationToken] const tokenConversionRate = tokenConversionRates[destinationToken]
const ethValueOfTrade = const conversionRateForSorting = tokenConversionRate || 1
destinationToken === ETH_SWAPS_TOKEN_ADDRESS
? calcTokenAmount(destinationAmount, 18).minus(totalEthCost, 10) const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
: new BigNumber(tokenConversionRate || 1, 10) conversionRateForSorting,
.times( 10,
calcTokenAmount( )
destinationAmount,
destinationTokenInfo.decimals, const conversionRateForCalculations =
), destinationToken === ETH_SWAPS_TOKEN_ADDRESS ? 1 : tokenConversionRate
10,
) const overallValueOfQuoteForSorting =
.minus(tokenConversionRate ? totalEthCost : 0, 10) conversionRateForCalculations === undefined
? ethValueOfTokens
// collect values for savings calculation : ethValueOfTokens.minus(ethFee, 10)
allEthTradeValues.push(ethValueOfTrade)
allEthFees.push(ethFee) quote.ethFee = ethFee.toString(10)
if (conversionRateForCalculations !== undefined) {
quote.ethValueOfTokens = ethValueOfTokens.toString(10)
quote.overallValueOfQuote = overallValueOfQuoteForSorting.toString(10)
quote.metaMaskFeeInEth = metaMaskFeeInTokens
.times(conversionRateForCalculations)
.toString(10)
}
if ( if (
ethTradeValueOfBestQuote === null || overallValueOfBestQuoteForSorting === null ||
ethValueOfTrade.gt(ethTradeValueOfBestQuote) overallValueOfQuoteForSorting.gt(overallValueOfBestQuoteForSorting)
) { ) {
topAggId = aggregator topAggId = aggregator
ethTradeValueOfBestQuote = ethValueOfTrade overallValueOfBestQuoteForSorting = overallValueOfQuoteForSorting
ethFeeForBestQuote = ethFee
} }
}) })
const isBest = const isBest =
quotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS || newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken]) Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken])
let savings = null let savings = null
if (isBest) { if (isBest) {
const bestQuote = newQuotes[topAggId]
savings = {} savings = {}
const {
ethFee: medianEthFee,
metaMaskFeeInEth: medianMetaMaskFee,
ethValueOfTokens: medianEthValueOfTokens,
} = getMedianEthValueQuote(Object.values(newQuotes))
// Performance savings are calculated as: // Performance savings are calculated as:
// valueForBestTrade - medianValueOfAllTrades // (ethValueOfTokens for the best trade) - (ethValueOfTokens for the media trade)
savings.performance = ethTradeValueOfBestQuote.minus( savings.performance = new BigNumber(bestQuote.ethValueOfTokens, 10).minus(
getMedian(allEthTradeValues), medianEthValueOfTokens,
10, 10,
) )
// Performance savings are calculated as: // Fee savings are calculated as:
// medianFeeOfAllTrades - feeForBestTrade // (fee for the median trade) - (fee for the best trade)
savings.fee = getMedian(allEthFees).minus(ethFeeForBestQuote, 10) savings.fee = new BigNumber(medianEthFee).minus(bestQuote.ethFee, 10)
savings.metaMaskFee = bestQuote.metaMaskFeeInEth
// Total savings are the sum of performance and fee savings // Total savings are calculated as:
savings.total = savings.performance.plus(savings.fee, 10).toString(10) // 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.performance = savings.performance.toString(10)
savings.fee = savings.fee.toString(10) savings.fee = savings.fee.toString(10)
savings.medianMetaMaskFee = medianMetaMaskFee
newQuotes[topAggId].isBestQuote = true
newQuotes[topAggId].savings = savings
} }
return { topAggId, isBest, savings } return [topAggId, newQuotes]
} }
async _getERC20Allowance(contractAddress, walletAddress) { async _getERC20Allowance(contractAddress, walletAddress) {
@ -685,34 +723,123 @@ export default class SwapsController {
} }
/** /**
* Calculates the median of a sample of BigNumber values. * Calculates the median overallValueOfQuote of a sample of quotes.
* *
* @param {import('bignumber.js').BigNumber[]} values - A sample of BigNumber * @param {Array} quotes - A sample of quote objects with overallValueOfQuote, ethFee, metaMaskFeeInEth, and ethValueOfTokens properties
* values. The array will be sorted in place. * @returns {Object} An object with the ethValueOfTokens, ethFee, and metaMaskFeeInEth of the quote with the median overallValueOfQuote
* @returns {import('bignumber.js').BigNumber} The median of the sample.
*/ */
function getMedian(values) { function getMedianEthValueQuote(_quotes) {
if (!Array.isArray(values) || values.length === 0) { if (!Array.isArray(_quotes) || _quotes.length === 0) {
throw new Error('Expected non-empty array param.') throw new Error('Expected non-empty array param.')
} }
values.sort((a, b) => { const quotes = [..._quotes]
if (a.equals(b)) {
quotes.sort((quoteA, quoteB) => {
const overallValueOfQuoteA = new BigNumber(quoteA.overallValueOfQuote, 10)
const overallValueOfQuoteB = new BigNumber(quoteB.overallValueOfQuote, 10)
if (overallValueOfQuoteA.equals(overallValueOfQuoteB)) {
return 0 return 0
} }
return a.lessThan(b) ? -1 : 1 return overallValueOfQuoteA.lessThan(overallValueOfQuoteB) ? -1 : 1
}) })
if (values.length % 2 === 1) { if (quotes.length % 2 === 1) {
// return middle value // return middle values
return values[(values.length - 1) / 2] const medianOverallValue =
quotes[(quotes.length - 1) / 2].overallValueOfQuote
const quotesMatchingMedianQuoteValue = quotes.filter(
(quote) => medianOverallValue === quote.overallValueOfQuote,
)
return meansOfQuotesFeesAndValue(quotesMatchingMedianQuoteValue)
} }
// return mean of middle two values // return mean of middle two values
const upperIndex = values.length / 2 const upperIndex = quotes.length / 2
return values[upperIndex].plus(values[upperIndex - 1]).dividedBy(2) const lowerIndex = upperIndex - 1
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)
.plus(feesAndValueAtLowerIndex.ethFee, 10)
.dividedBy(2)
.toString(10),
metaMaskFeeInEth: new BigNumber(
feesAndValueAtUpperIndex.metaMaskFeeInEth,
10,
)
.plus(feesAndValueAtLowerIndex.metaMaskFeeInEth, 10)
.dividedBy(2)
.toString(10),
ethValueOfTokens: new BigNumber(
feesAndValueAtUpperIndex.ethValueOfTokens,
10,
)
.plus(feesAndValueAtLowerIndex.ethValueOfTokens, 10)
.dividedBy(2)
.toString(10),
}
}
/**
* Calculates the arithmetic mean for each of three properties - ethFee, metaMaskFeeInEth and ethValueOfTokens - across
* an array of objects containing those properties.
*
* @param {Array} quotes - A sample of quote objects with overallValueOfQuote, ethFee, metaMaskFeeInEth and
* ethValueOfTokens properties
* @returns {Object} An object with the arithmetic mean each of the ethFee, metaMaskFeeInEth and ethValueOfTokens of
* the passed quote objects
*/
function meansOfQuotesFeesAndValue(quotes) {
const feeAndValueSumsAsBigNumbers = quotes.reduce(
(feeAndValueSums, quote) => ({
ethFee: feeAndValueSums.ethFee.plus(quote.ethFee, 10),
metaMaskFeeInEth: feeAndValueSums.metaMaskFeeInEth.plus(
quote.metaMaskFeeInEth,
10,
),
ethValueOfTokens: feeAndValueSums.ethValueOfTokens.plus(
quote.ethValueOfTokens,
10,
),
}),
{
ethFee: new BigNumber(0, 10),
metaMaskFeeInEth: new BigNumber(0, 10),
ethValueOfTokens: new BigNumber(0, 10),
},
)
return {
ethFee: feeAndValueSumsAsBigNumbers.ethFee
.div(quotes.length, 10)
.toString(10),
metaMaskFeeInEth: feeAndValueSumsAsBigNumbers.metaMaskFeeInEth
.div(quotes.length, 10)
.toString(10),
ethValueOfTokens: feeAndValueSumsAsBigNumbers.ethValueOfTokens
.div(quotes.length, 10)
.toString(10),
}
} }
export const utils = { export const utils = {
getMedian, getMedianEthValueQuote,
meansOfQuotesFeesAndValue,
} }

@ -2,12 +2,14 @@ import assert from 'assert'
import sinon from 'sinon' import sinon from 'sinon'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { mapValues } from 'lodash'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
import { import {
ROPSTEN_NETWORK_ID, ROPSTEN_NETWORK_ID,
MAINNET_NETWORK_ID, MAINNET_NETWORK_ID,
} from '../../../../app/scripts/controllers/network/enums' } from '../../../../app/scripts/controllers/network/enums'
import { ETH_SWAPS_TOKEN_ADDRESS } from '../../../../ui/app/helpers/constants/swaps'
import { createTestProviderTools } from '../../../stub/provider' import { createTestProviderTools } from '../../../stub/provider'
import SwapsController, { import SwapsController, {
utils, utils,
@ -25,6 +27,10 @@ const MOCK_FETCH_PARAMS = {
const TEST_AGG_ID_1 = 'TEST_AGG_1' const TEST_AGG_ID_1 = 'TEST_AGG_1'
const TEST_AGG_ID_2 = 'TEST_AGG_2' const TEST_AGG_ID_2 = 'TEST_AGG_2'
const TEST_AGG_ID_3 = 'TEST_AGG_3'
const TEST_AGG_ID_4 = 'TEST_AGG_4'
const TEST_AGG_ID_5 = 'TEST_AGG_5'
const TEST_AGG_ID_6 = 'TEST_AGG_6'
const TEST_AGG_ID_BEST = 'TEST_AGG_BEST' const TEST_AGG_ID_BEST = 'TEST_AGG_BEST'
const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL' const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL'
@ -61,6 +67,7 @@ const MOCK_QUOTES_APPROVAL_REQUIRED = {
aggType: 'AGG', aggType: 'AGG',
slippage: 3, slippage: 3,
approvalNeeded: MOCK_APPROVAL_NEEDED, approvalNeeded: MOCK_APPROVAL_NEEDED,
fee: 1,
}, },
} }
@ -72,7 +79,10 @@ const MOCK_FETCH_METADATA = {
} }
const MOCK_TOKEN_RATES_STORE = new ObservableStore({ const MOCK_TOKEN_RATES_STORE = new ObservableStore({
contractExchangeRates: { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2 }, contractExchangeRates: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,
'0x1111111111111111111111111111111111111111': 0.1,
},
}) })
const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' }) const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' })
@ -350,6 +360,13 @@ describe('SwapsController', function () {
}) })
describe('_findTopQuoteAndCalculateSavings', function () { describe('_findTopQuoteAndCalculateSavings', function () {
beforeEach(function () {
const { swapsState } = swapsController.store.getState()
swapsController.store.updateState({
swapsState: { ...swapsState, customGasPrice: '0x174876e800' },
})
})
it('returns empty object if passed undefined or empty object', async function () { it('returns empty object if passed undefined or empty object', async function () {
assert.deepStrictEqual( assert.deepStrictEqual(
await swapsController._findTopQuoteAndCalculateSavings(), await swapsController._findTopQuoteAndCalculateSavings(),
@ -360,6 +377,261 @@ describe('SwapsController', function () {
{}, {},
) )
}) })
it('returns the top aggId and quotes with savings and fee values if passed necessary data and an even number of quotes', async function () {
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(
getTopQuoteAndSavingsMockQuotes(),
)
assert.equal(topAggId, TEST_AGG_ID_1)
assert.deepStrictEqual(
resultQuotes,
getTopQuoteAndSavingsBaseExpectedResults(),
)
})
it('returns the top aggId and quotes with savings and fee values if passed necessary data and an odd number of quotes', async function () {
const testInput = getTopQuoteAndSavingsMockQuotes()
delete testInput[TEST_AGG_ID_6]
const expectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults()
delete expectedResultQuotes[TEST_AGG_ID_6]
expectedResultQuotes[TEST_AGG_ID_1].savings = {
total: '0.0292',
performance: '0.0297',
fee: '0.02',
metaMaskFee: '0.0205',
medianMetaMaskFee: '0.0202',
}
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(testInput)
assert.equal(topAggId, TEST_AGG_ID_1)
assert.deepStrictEqual(resultQuotes, expectedResultQuotes)
})
it('returns the top aggId, without best quote flagged, and quotes with fee values if passed necessary data but no custom convert rate exists', async function () {
const testInput = mapValues(
getTopQuoteAndSavingsMockQuotes(),
(quote) => ({
...quote,
destinationToken: '0xnoConversionRateExists',
}),
)
const expectedResultQuotes = {
[TEST_AGG_ID_1]: {
...testInput[TEST_AGG_ID_1],
ethFee: '0.01',
},
[TEST_AGG_ID_2]: {
...testInput[TEST_AGG_ID_2],
ethFee: '0.02',
},
[TEST_AGG_ID_3]: {
...testInput[TEST_AGG_ID_3],
ethFee: '0.03',
},
[TEST_AGG_ID_4]: {
...testInput[TEST_AGG_ID_4],
ethFee: '0.04',
},
[TEST_AGG_ID_5]: {
...testInput[TEST_AGG_ID_5],
ethFee: '0.05',
},
[TEST_AGG_ID_6]: {
...testInput[TEST_AGG_ID_6],
ethFee: '0.06',
},
}
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(testInput)
assert.equal(topAggId, TEST_AGG_ID_1)
assert.deepStrictEqual(resultQuotes, expectedResultQuotes)
})
it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is ETH', async function () {
const testInput = mapValues(
getTopQuoteAndSavingsMockQuotes(),
(quote) => ({
...quote,
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
}),
)
const baseExpectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults()
const expectedResultQuotes = {
[TEST_AGG_ID_1]: {
...baseExpectedResultQuotes[TEST_AGG_ID_1],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '2.0195',
},
[TEST_AGG_ID_2]: {
...baseExpectedResultQuotes[TEST_AGG_ID_2],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9996',
},
[TEST_AGG_ID_3]: {
...baseExpectedResultQuotes[TEST_AGG_ID_3],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9698',
},
[TEST_AGG_ID_4]: {
...baseExpectedResultQuotes[TEST_AGG_ID_4],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.94',
},
[TEST_AGG_ID_5]: {
...baseExpectedResultQuotes[TEST_AGG_ID_5],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9102',
},
[TEST_AGG_ID_6]: {
...baseExpectedResultQuotes[TEST_AGG_ID_6],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.8705',
},
}
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(testInput)
assert.equal(topAggId, TEST_AGG_ID_1)
assert.deepStrictEqual(resultQuotes, expectedResultQuotes)
})
it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is ETH and an ETH fee is included in the trade value of what would be the best quote', async function () {
const testInput = mapValues(
getTopQuoteAndSavingsMockQuotes(),
(quote) => ({
...quote,
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
}),
)
// 0.04 ETH fee included in trade value
testInput[TEST_AGG_ID_1].trade.value = '0x8b553ece48ec0000'
const baseExpectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults()
const expectedResultQuotes = {
[TEST_AGG_ID_1]: {
...baseExpectedResultQuotes[TEST_AGG_ID_1],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8b553ece48ec0000' },
overallValueOfQuote: '1.9795',
ethFee: '0.05',
},
[TEST_AGG_ID_2]: {
...baseExpectedResultQuotes[TEST_AGG_ID_2],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9996',
isBestQuote: true,
savings: {
total: '0.0243',
performance: '0.0297',
fee: '0.015',
metaMaskFee: '0.0204',
medianMetaMaskFee: '0.0201',
},
},
[TEST_AGG_ID_3]: {
...baseExpectedResultQuotes[TEST_AGG_ID_3],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9698',
},
[TEST_AGG_ID_4]: {
...baseExpectedResultQuotes[TEST_AGG_ID_4],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.94',
},
[TEST_AGG_ID_5]: {
...baseExpectedResultQuotes[TEST_AGG_ID_5],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.9102',
},
[TEST_AGG_ID_6]: {
...baseExpectedResultQuotes[TEST_AGG_ID_6],
sourceToken: ETH_SWAPS_TOKEN_ADDRESS,
destinationToken: '0x1111111111111111111111111111111111111111',
trade: { value: '0x8ac7230489e80000' },
overallValueOfQuote: '1.8705',
},
}
delete expectedResultQuotes[TEST_AGG_ID_1].isBestQuote
delete expectedResultQuotes[TEST_AGG_ID_1].savings
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(testInput)
assert.equal(topAggId, TEST_AGG_ID_2)
assert.deepStrictEqual(resultQuotes, expectedResultQuotes)
})
it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is not ETH and an ETH fee is included in the trade value of what would be the best quote', async function () {
const testInput = getTopQuoteAndSavingsMockQuotes()
// 0.04 ETH fee included in trade value
testInput[TEST_AGG_ID_1].trade.value = '0x8e1bc9bf040000'
const baseExpectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults()
const expectedResultQuotes = {
...baseExpectedResultQuotes,
[TEST_AGG_ID_1]: {
...baseExpectedResultQuotes[TEST_AGG_ID_1],
trade: { value: '0x8e1bc9bf040000' },
overallValueOfQuote: '1.9795',
ethFee: '0.05',
},
[TEST_AGG_ID_2]: {
...baseExpectedResultQuotes[TEST_AGG_ID_2],
isBestQuote: true,
savings: {
total: '0.0243',
performance: '0.0297',
fee: '0.015',
metaMaskFee: '0.0204',
medianMetaMaskFee: '0.0201',
},
},
}
delete expectedResultQuotes[TEST_AGG_ID_1].isBestQuote
delete expectedResultQuotes[TEST_AGG_ID_1].savings
const [
topAggId,
resultQuotes,
] = await swapsController._findTopQuoteAndCalculateSavings(testInput)
assert.equal(topAggId, [TEST_AGG_ID_2])
assert.deepStrictEqual(resultQuotes, expectedResultQuotes)
})
}) })
describe('fetchAndSetQuotes', function () { describe('fetchAndSetQuotes', function () {
@ -394,9 +666,15 @@ describe('SwapsController', function () {
gasEstimateWithRefund: 'b8cae', gasEstimateWithRefund: 'b8cae',
savings: { savings: {
fee: '0', fee: '0',
metaMaskFee: '0.5050505050505050505',
performance: '6', performance: '6',
total: '6', total: '5.4949494949494949495',
medianMetaMaskFee: '0.44444444444444444444',
}, },
ethFee: '33554432',
overallValueOfQuote: '-33554382',
metaMaskFeeInEth: '0.5050505050505050505',
ethValueOfTokens: '50',
}) })
assert.strictEqual( assert.strictEqual(
@ -515,7 +793,7 @@ describe('SwapsController', function () {
MOCK_FETCH_METADATA, MOCK_FETCH_METADATA,
) )
assert.strictEqual(newQuotes[topAggId].isBestQuote, false) assert.strictEqual(newQuotes[topAggId].isBestQuote, undefined)
}) })
}) })
@ -865,37 +1143,215 @@ describe('SwapsController', function () {
}) })
describe('utils', function () { describe('utils', function () {
describe('getMedian', function () { describe('getMedianEthValueQuote', function () {
const { getMedian } = utils const { getMedianEthValueQuote } = utils
it('calculates median correctly with uneven sample', function () { it('calculates median correctly with uneven sample', function () {
const values = [3, 2, 6].map((value) => new BigNumber(value)) const expectedResult = {
const median = getMedian(values) ethFee: '10',
metaMaskFeeInEth: '5',
ethValueOfTokens: '0.3',
}
const values = [
{
overallValueOfQuote: '3',
ethFee: '10',
metaMaskFeeInEth: '5',
ethValueOfTokens: '0.3',
},
{
overallValueOfQuote: '2',
ethFee: '20',
metaMaskFeeInEth: '3',
ethValueOfTokens: '0.2',
},
{
overallValueOfQuote: '6',
ethFee: '40',
metaMaskFeeInEth: '6',
ethValueOfTokens: '0.6',
},
]
assert.strictEqual( const median = getMedianEthValueQuote(values)
median.toNumber(),
3, assert.deepEqual(
'should have returned correct median', median,
expectedResult,
'should have returned correct median quote object',
) )
}) })
it('calculates median correctly with even sample', function () { it('calculates median correctly with even sample', function () {
const values = [3, 2, 2, 6].map((value) => new BigNumber(value)) const expectedResult = {
const median = getMedian(values) ethFee: '20',
metaMaskFeeInEth: '6.5',
ethValueOfTokens: '0.25',
}
const values = [
{
overallValueOfQuote: '3',
ethFee: '10',
metaMaskFeeInEth: '5',
ethValueOfTokens: '0.3',
},
{
overallValueOfQuote: '1',
ethFee: '20',
metaMaskFeeInEth: '3',
ethValueOfTokens: '0.2',
},
{
overallValueOfQuote: '2',
ethFee: '30',
metaMaskFeeInEth: '8',
ethValueOfTokens: '0.2',
},
{
overallValueOfQuote: '6',
ethFee: '40',
metaMaskFeeInEth: '6',
ethValueOfTokens: '0.6',
},
]
const median = getMedianEthValueQuote(values)
assert.strictEqual( assert.deepEqual(
median.toNumber(), median,
2.5, expectedResult,
'should have returned correct median', 'should have returned correct median quote object',
)
})
it('calculates median correctly with an uneven sample where multiple quotes have the median overall value', function () {
const expectedResult = {
ethFee: '2',
metaMaskFeeInEth: '0.5',
ethValueOfTokens: '5',
}
const values = [
{
overallValueOfQuote: '1',
ethValueOfTokens: '2',
ethFee: '1',
metaMaskFeeInEth: '0.2',
},
{
overallValueOfQuote: '3',
ethValueOfTokens: '4',
ethFee: '1',
metaMaskFeeInEth: '0.4',
},
{
overallValueOfQuote: '3',
ethValueOfTokens: '5',
ethFee: '2',
metaMaskFeeInEth: '0.5',
},
{
overallValueOfQuote: '3',
ethValueOfTokens: '6',
ethFee: '3',
metaMaskFeeInEth: '0.6',
},
{
overallValueOfQuote: '4',
ethValueOfTokens: '6',
ethFee: '2',
metaMaskFeeInEth: '0.6',
},
{
overallValueOfQuote: '4',
ethValueOfTokens: '7',
ethFee: '3',
metaMaskFeeInEth: '0.7',
},
{
overallValueOfQuote: '6',
ethValueOfTokens: '8',
ethFee: '2',
metaMaskFeeInEth: '0.8',
},
]
const median = getMedianEthValueQuote(values)
assert.deepEqual(
median,
expectedResult,
'should have returned correct median quote object',
)
})
it('calculates median correctly with an even sample where multiple quotes have the same overall value as either of the two middle values', function () {
const expectedResult = {
ethFee: '2',
metaMaskFeeInEth: '0.55',
ethValueOfTokens: '5.5',
}
const values = [
{
overallValueOfQuote: '1',
ethValueOfTokens: '2',
ethFee: '1',
metaMaskFeeInEth: '0.2',
},
{
overallValueOfQuote: '3',
ethValueOfTokens: '4',
ethFee: '1',
metaMaskFeeInEth: '0.4',
},
{
overallValueOfQuote: '3',
ethValueOfTokens: '5',
ethFee: '2',
metaMaskFeeInEth: '0.5',
},
{
overallValueOfQuote: '4',
ethValueOfTokens: '6',
ethFee: '2',
metaMaskFeeInEth: '0.6',
},
{
overallValueOfQuote: '4',
ethValueOfTokens: '7',
ethFee: '3',
metaMaskFeeInEth: '0.7',
},
{
overallValueOfQuote: '6',
ethValueOfTokens: '8',
ethFee: '2',
metaMaskFeeInEth: '0.8',
},
]
const median = getMedianEthValueQuote(values)
assert.deepEqual(
median,
expectedResult,
'should have returned correct median quote object',
) )
}) })
it('throws on empty or non-array sample', function () { it('throws on empty or non-array sample', function () {
assert.throws(() => getMedian([]), 'should throw on empty array') assert.throws(
() => getMedianEthValueQuote([]),
'should throw on empty array',
)
assert.throws(() => getMedian(), 'should throw on non-array param') assert.throws(
() => getMedianEthValueQuote(),
'should throw on non-array param',
)
assert.throws(() => getMedian({}), 'should throw on non-array param') assert.throws(
() => getMedianEthValueQuote({}),
'should throw on non-array param',
)
}) })
}) })
}) })
@ -934,6 +1390,7 @@ function getMockQuotes() {
symbol: 'USDC', symbol: 'USDC',
decimals: 18, decimals: 18,
}, },
fee: 1,
}, },
[TEST_AGG_ID_BEST]: { [TEST_AGG_ID_BEST]: {
@ -967,6 +1424,7 @@ function getMockQuotes() {
symbol: 'USDC', symbol: 'USDC',
decimals: 18, decimals: 18,
}, },
fee: 1,
}, },
[TEST_AGG_ID_2]: { [TEST_AGG_ID_2]: {
@ -1000,6 +1458,160 @@ function getMockQuotes() {
symbol: 'USDC', symbol: 'USDC',
decimals: 18, decimals: 18,
}, },
fee: 1,
},
}
}
function getTopQuoteAndSavingsMockQuotes() {
// These destination amounts are calculated using the following "pre-fee" amounts
// TEST_AGG_ID_1: 20.5
// TEST_AGG_ID_2: 20.4
// TEST_AGG_ID_3: 20.2
// TEST_AGG_ID_4: 20
// TEST_AGG_ID_5: 19.8
// TEST_AGG_ID_6: 19.5
return {
[TEST_AGG_ID_1]: {
aggregator: TEST_AGG_ID_1,
approvalNeeded: null,
gasEstimate: '0x186a0',
destinationAmount: '20295000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
[TEST_AGG_ID_2]: {
aggregator: TEST_AGG_ID_2,
approvalNeeded: null,
gasEstimate: '0x30d40',
destinationAmount: '20196000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
[TEST_AGG_ID_3]: {
aggregator: TEST_AGG_ID_3,
approvalNeeded: null,
gasEstimate: '0x493e0',
destinationAmount: '19998000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
[TEST_AGG_ID_4]: {
aggregator: TEST_AGG_ID_4,
approvalNeeded: null,
gasEstimate: '0x61a80',
destinationAmount: '19800000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
[TEST_AGG_ID_5]: {
aggregator: TEST_AGG_ID_5,
approvalNeeded: null,
gasEstimate: '0x7a120',
destinationAmount: '19602000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
[TEST_AGG_ID_6]: {
aggregator: TEST_AGG_ID_6,
approvalNeeded: null,
gasEstimate: '0x927c0',
destinationAmount: '19305000000000000000',
destinationToken: '0x1111111111111111111111111111111111111111',
destinationTokenInfo: { decimals: 18 },
sourceAmount: '10000000000000000000',
sourceToken: '0xsomeERC20TokenAddress',
trade: {
value: '0x0',
},
fee: 1,
},
}
}
function getTopQuoteAndSavingsBaseExpectedResults() {
const baseTestInput = getTopQuoteAndSavingsMockQuotes()
return {
[TEST_AGG_ID_1]: {
...baseTestInput[TEST_AGG_ID_1],
isBestQuote: true,
ethFee: '0.01',
overallValueOfQuote: '2.0195',
metaMaskFeeInEth: '0.0205',
ethValueOfTokens: '2.0295',
savings: {
total: '0.0441',
performance: '0.0396',
fee: '0.025',
metaMaskFee: '0.0205',
medianMetaMaskFee: '0.0201',
},
},
[TEST_AGG_ID_2]: {
...baseTestInput[TEST_AGG_ID_2],
ethFee: '0.02',
overallValueOfQuote: '1.9996',
metaMaskFeeInEth: '0.0204',
ethValueOfTokens: '2.0196',
},
[TEST_AGG_ID_3]: {
...baseTestInput[TEST_AGG_ID_3],
ethFee: '0.03',
overallValueOfQuote: '1.9698',
metaMaskFeeInEth: '0.0202',
ethValueOfTokens: '1.9998',
},
[TEST_AGG_ID_4]: {
...baseTestInput[TEST_AGG_ID_4],
ethFee: '0.04',
overallValueOfQuote: '1.94',
metaMaskFeeInEth: '0.02',
ethValueOfTokens: '1.98',
},
[TEST_AGG_ID_5]: {
...baseTestInput[TEST_AGG_ID_5],
ethFee: '0.05',
overallValueOfQuote: '1.9102',
metaMaskFeeInEth: '0.0198',
ethValueOfTokens: '1.9602',
},
[TEST_AGG_ID_6]: {
...baseTestInput[TEST_AGG_ID_6],
ethFee: '0.06',
overallValueOfQuote: '1.8705',
metaMaskFeeInEth: '0.0195',
ethValueOfTokens: '1.9305',
}, },
} }
} }

Loading…
Cancel
Save