mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Add swaps savings calculation (#9611)
* Calculate savings per swap relative to median values * Update test mock quotes, add getMedian tests * Identify assets by sourceToken and destinationToken
This commit is contained in:
parent
43d97ebe4f
commit
7de7e7d8c2
@ -175,11 +175,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 topAggData = await this._findTopQuoteAggId(newQuotes)
|
const topQuoteData = await this._findTopQuoteAndCalculateSavings(newQuotes)
|
||||||
|
|
||||||
if (topAggData.topAggId) {
|
if (topQuoteData.topAggId) {
|
||||||
topAggId = topAggData.topAggId
|
topAggId = topQuoteData.topAggId
|
||||||
newQuotes[topAggId].isBestQuote = topAggData.isBest
|
newQuotes[topAggId].isBestQuote = topQuoteData.isBest
|
||||||
|
newQuotes[topAggId].savings = topQuoteData.savings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,48 +395,61 @@ export default class SwapsController {
|
|||||||
return ethersGasPrice.toHexString()
|
return ethersGasPrice.toHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
async _findTopQuoteAggId (quotes) {
|
async _findTopQuoteAndCalculateSavings (quotes = {}) {
|
||||||
const tokenConversionRates = this.tokenRatesStore.getState()
|
const tokenConversionRates = this.tokenRatesStore.getState()
|
||||||
.contractExchangeRates
|
.contractExchangeRates
|
||||||
const {
|
const {
|
||||||
swapsState: { customGasPrice },
|
swapsState: { customGasPrice },
|
||||||
} = this.store.getState()
|
} = this.store.getState()
|
||||||
|
|
||||||
if (!Object.values(quotes).length) {
|
const numQuotes = Object.keys(quotes).length
|
||||||
|
if (!numQuotes) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const usedGasPrice = customGasPrice || await this._getEthersGasPrice()
|
const usedGasPrice = customGasPrice || await this._getEthersGasPrice()
|
||||||
|
|
||||||
let topAggId = ''
|
let topAggId = ''
|
||||||
let ethValueOfTradeForBestQuote = null
|
let ethTradeValueOfBestQuote = null
|
||||||
|
let ethFeeForBestQuote = null
|
||||||
|
const allEthTradeValues = []
|
||||||
|
const allEthFees = []
|
||||||
|
|
||||||
Object.values(quotes).forEach((quote) => {
|
Object.values(quotes).forEach((quote) => {
|
||||||
const {
|
const {
|
||||||
|
aggregator,
|
||||||
|
approvalNeeded,
|
||||||
|
averageGas,
|
||||||
destinationAmount = 0,
|
destinationAmount = 0,
|
||||||
destinationToken,
|
destinationToken,
|
||||||
destinationTokenInfo,
|
destinationTokenInfo,
|
||||||
trade,
|
|
||||||
approvalNeeded,
|
|
||||||
averageGas,
|
|
||||||
gasEstimate,
|
gasEstimate,
|
||||||
aggregator,
|
sourceAmount,
|
||||||
|
sourceToken,
|
||||||
|
trade,
|
||||||
} = quote
|
} = quote
|
||||||
|
|
||||||
const tradeGasLimitForCalculation = gasEstimate
|
const tradeGasLimitForCalculation = gasEstimate
|
||||||
? new BigNumber(gasEstimate, 16)
|
? new BigNumber(gasEstimate, 16)
|
||||||
: new BigNumber(averageGas || MAX_GAS_LIMIT, 10)
|
: new BigNumber(averageGas || MAX_GAS_LIMIT, 10)
|
||||||
|
|
||||||
const totalGasLimitForCalculation = tradeGasLimitForCalculation
|
const totalGasLimitForCalculation = tradeGasLimitForCalculation
|
||||||
.plus(approvalNeeded?.gas || '0x0', 16)
|
.plus(approvalNeeded?.gas || '0x0', 16)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
|
|
||||||
const gasTotalInWeiHex = calcGasTotal(
|
const gasTotalInWeiHex = calcGasTotal(
|
||||||
totalGasLimitForCalculation,
|
totalGasLimitForCalculation,
|
||||||
usedGasPrice,
|
usedGasPrice,
|
||||||
)
|
)
|
||||||
const totalEthCost = new BigNumber(gasTotalInWeiHex, 16).plus(
|
|
||||||
trade.value,
|
// trade.value is a sum of different values depending on the transaction.
|
||||||
16,
|
// It always includes any external fees charged by the quote source. In
|
||||||
)
|
// addition, if the source asset is ETH, trade.value includes the amount
|
||||||
const ethFee = conversionUtil(totalEthCost, {
|
// of swapped ETH.
|
||||||
|
const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16)
|
||||||
|
.plus(trade.value, 16)
|
||||||
|
|
||||||
|
const totalEthCost = conversionUtil(totalWeiCost, {
|
||||||
fromCurrency: 'ETH',
|
fromCurrency: 'ETH',
|
||||||
fromDenomination: 'WEI',
|
fromDenomination: 'WEI',
|
||||||
toDenomination: 'ETH',
|
toDenomination: 'ETH',
|
||||||
@ -443,10 +457,26 @@ export default class SwapsController {
|
|||||||
numberOfDecimals: 6,
|
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.
|
||||||
|
// Otherwise, the total fee is simply trade.value plus gas fees.
|
||||||
|
const ethFee = sourceToken === ETH_SWAPS_TOKEN_ADDRESS
|
||||||
|
? conversionUtil(
|
||||||
|
totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei
|
||||||
|
{
|
||||||
|
fromCurrency: 'ETH',
|
||||||
|
fromDenomination: 'WEI',
|
||||||
|
toDenomination: 'ETH',
|
||||||
|
fromNumericBase: 'BN',
|
||||||
|
numberOfDecimals: 6,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: totalEthCost
|
||||||
|
|
||||||
const tokenConversionRate = tokenConversionRates[destinationToken]
|
const tokenConversionRate = tokenConversionRates[destinationToken]
|
||||||
const ethValueOfTrade =
|
const ethValueOfTrade =
|
||||||
destinationTokenInfo.symbol === 'ETH'
|
destinationToken === ETH_SWAPS_TOKEN_ADDRESS
|
||||||
? calcTokenAmount(destinationAmount, 18).minus(ethFee, 10)
|
? calcTokenAmount(destinationAmount, 18).minus(totalEthCost, 10)
|
||||||
: new BigNumber(tokenConversionRate || 1, 10)
|
: new BigNumber(tokenConversionRate || 1, 10)
|
||||||
.times(
|
.times(
|
||||||
calcTokenAmount(
|
calcTokenAmount(
|
||||||
@ -455,22 +485,51 @@ export default class SwapsController {
|
|||||||
),
|
),
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
.minus(tokenConversionRate ? ethFee.toString(10) : 0, 10)
|
.minus(tokenConversionRate ? totalEthCost : 0, 10)
|
||||||
|
|
||||||
|
// collect values for savings calculation
|
||||||
|
allEthTradeValues.push(ethValueOfTrade)
|
||||||
|
allEthFees.push(ethFee)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ethValueOfTradeForBestQuote === null ||
|
ethTradeValueOfBestQuote === null ||
|
||||||
ethValueOfTrade.gt(ethValueOfTradeForBestQuote)
|
ethValueOfTrade.gt(ethTradeValueOfBestQuote)
|
||||||
) {
|
) {
|
||||||
topAggId = aggregator
|
topAggId = aggregator
|
||||||
ethValueOfTradeForBestQuote = ethValueOfTrade
|
ethTradeValueOfBestQuote = ethValueOfTrade
|
||||||
|
ethFeeForBestQuote = ethFee
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isBest =
|
const isBest =
|
||||||
quotes[topAggId]?.destinationTokenInfo?.symbol === 'ETH' ||
|
quotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_ADDRESS ||
|
||||||
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken])
|
Boolean(tokenConversionRates[quotes[topAggId]?.destinationToken])
|
||||||
|
|
||||||
return { topAggId, isBest }
|
let savings = null
|
||||||
|
|
||||||
|
if (isBest) {
|
||||||
|
savings = {}
|
||||||
|
// Performance savings are calculated as:
|
||||||
|
// valueForBestTrade - medianValueOfAllTrades
|
||||||
|
savings.performance = ethTradeValueOfBestQuote.minus(
|
||||||
|
getMedian(allEthTradeValues),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Performance savings are calculated as:
|
||||||
|
// medianFeeOfAllTrades - feeForBestTrade
|
||||||
|
savings.fee = getMedian(allEthFees).minus(
|
||||||
|
ethFeeForBestQuote,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Total savings are the sum of performance and fee savings
|
||||||
|
savings.total = savings.performance.plus(savings.fee, 10).toString(10)
|
||||||
|
savings.performance = savings.performance.toString(10)
|
||||||
|
savings.fee = savings.fee.toString(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { topAggId, isBest, savings }
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getERC20Allowance (contractAddress, walletAddress) {
|
async _getERC20Allowance (contractAddress, walletAddress) {
|
||||||
@ -563,5 +622,39 @@ export default class SwapsController {
|
|||||||
this.setSwapsLiveness(swapsFeatureIsLive)
|
this.setSwapsLiveness(swapsFeatureIsLive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the median of a sample of BigNumber values.
|
||||||
|
*
|
||||||
|
* @param {import('bignumber.js').BigNumber[]} values - A sample of BigNumber
|
||||||
|
* values. The array will be sorted in place.
|
||||||
|
* @returns {import('bignumber.js').BigNumber} The median of the sample.
|
||||||
|
*/
|
||||||
|
function getMedian (values) {
|
||||||
|
if (!Array.isArray(values) || values.length === 0) {
|
||||||
|
throw new Error('Expected non-empty array param.')
|
||||||
|
}
|
||||||
|
|
||||||
|
values.sort((a, b) => {
|
||||||
|
if (a.equals(b)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a.lessThan(b) ? -1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (values.length % 2 === 1) {
|
||||||
|
// return middle value
|
||||||
|
return values[(values.length - 1) / 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// return mean of middle two values
|
||||||
|
const upperIndex = values.length / 2
|
||||||
|
return values[upperIndex]
|
||||||
|
.plus(values[upperIndex - 1])
|
||||||
|
.dividedBy(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const utils = {
|
||||||
|
getMedian,
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { ethers } from 'ethers'
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import ObservableStore from 'obs-store'
|
import ObservableStore from 'obs-store'
|
||||||
import { createTestProviderTools } from '../../../stub/provider'
|
import { createTestProviderTools } from '../../../stub/provider'
|
||||||
import SwapsController from '../../../../app/scripts/controllers/swaps'
|
import SwapsController, { utils } from '../../../../app/scripts/controllers/swaps'
|
||||||
|
|
||||||
const MOCK_FETCH_PARAMS = {
|
const MOCK_FETCH_PARAMS = {
|
||||||
slippage: 3,
|
slippage: 3,
|
||||||
@ -17,32 +17,11 @@ const MOCK_FETCH_PARAMS = {
|
|||||||
exchangeList: 'zeroExV1',
|
exchangeList: 'zeroExV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_AGG_ID = 'zeroExV1'
|
const TEST_AGG_ID_1 = 'TEST_AGG_1'
|
||||||
const MOCK_QUOTES = {
|
const TEST_AGG_ID_2 = 'TEST_AGG_2'
|
||||||
[TEST_AGG_ID]: {
|
const TEST_AGG_ID_BEST = 'TEST_AGG_BEST'
|
||||||
trade: {
|
const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL'
|
||||||
data: '0x00',
|
|
||||||
from: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078',
|
|
||||||
value: '0x17647444f166000',
|
|
||||||
gas: '0xe09c0',
|
|
||||||
gasPrice: undefined,
|
|
||||||
to: '0x881d40237659c251811cec9c364ef91dc08d300c',
|
|
||||||
},
|
|
||||||
sourceAmount: '1000000000000000000000000000000000000',
|
|
||||||
destinationAmount: '396493201125465',
|
|
||||||
error: null,
|
|
||||||
sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
|
||||||
destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
||||||
approvalNeeded: null,
|
|
||||||
maxGas: 920000,
|
|
||||||
averageGas: 312510,
|
|
||||||
estimatedRefund: 343090,
|
|
||||||
fetchTime: 559,
|
|
||||||
aggregator: TEST_AGG_ID,
|
|
||||||
aggType: 'AGG',
|
|
||||||
slippage: 3,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const MOCK_APPROVAL_NEEDED = {
|
const MOCK_APPROVAL_NEEDED = {
|
||||||
'data': '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00',
|
'data': '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00',
|
||||||
'to': '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
'to': '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||||
@ -51,8 +30,9 @@ const MOCK_APPROVAL_NEEDED = {
|
|||||||
'gas': '12',
|
'gas': '12',
|
||||||
'gasPrice': '34',
|
'gasPrice': '34',
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOCK_QUOTES_APPROVAL_REQUIRED = {
|
const MOCK_QUOTES_APPROVAL_REQUIRED = {
|
||||||
[TEST_AGG_ID]: {
|
[TEST_AGG_ID_APPROVAL]: {
|
||||||
trade: {
|
trade: {
|
||||||
data: '0x00',
|
data: '0x00',
|
||||||
from: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078',
|
from: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078',
|
||||||
@ -70,12 +50,13 @@ const MOCK_QUOTES_APPROVAL_REQUIRED = {
|
|||||||
averageGas: 312510,
|
averageGas: 312510,
|
||||||
estimatedRefund: 343090,
|
estimatedRefund: 343090,
|
||||||
fetchTime: 559,
|
fetchTime: 559,
|
||||||
aggregator: TEST_AGG_ID,
|
aggregator: TEST_AGG_ID_APPROVAL,
|
||||||
aggType: 'AGG',
|
aggType: 'AGG',
|
||||||
slippage: 3,
|
slippage: 3,
|
||||||
approvalNeeded: MOCK_APPROVAL_NEEDED,
|
approvalNeeded: MOCK_APPROVAL_NEEDED,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOCK_FETCH_METADATA = {
|
const MOCK_FETCH_METADATA = {
|
||||||
destinationTokenInfo: {
|
destinationTokenInfo: {
|
||||||
symbol: 'FOO',
|
symbol: 'FOO',
|
||||||
@ -233,14 +214,14 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should set initial gas estimate', async function () {
|
it('should set initial gas estimate', async function () {
|
||||||
const initialAggId = TEST_AGG_ID
|
const initialAggId = TEST_AGG_ID_1
|
||||||
const baseGasEstimate = 10
|
const baseGasEstimate = 10
|
||||||
const { maxGas, estimatedRefund } = MOCK_QUOTES[TEST_AGG_ID]
|
const { maxGas, estimatedRefund } = getMockQuotes()[TEST_AGG_ID_1]
|
||||||
|
|
||||||
const { swapsState } = swapsController.store.getState()
|
const { swapsState } = swapsController.store.getState()
|
||||||
// Set mock quotes in order to have data for the test agg
|
// Set mock quotes in order to have data for the test agg
|
||||||
swapsController.store.updateState({
|
swapsController.store.updateState({
|
||||||
swapsState: { ...swapsState, quotes: MOCK_QUOTES },
|
swapsState: { ...swapsState, quotes: getMockQuotes() },
|
||||||
})
|
})
|
||||||
|
|
||||||
await swapsController.setInitialGasEstimate(
|
await swapsController.setInitialGasEstimate(
|
||||||
@ -272,6 +253,19 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('_findTopQuoteAndCalculateSavings', function () {
|
||||||
|
it('returns empty object if passed undefined or empty object', async function () {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await swapsController._findTopQuoteAndCalculateSavings(),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
await swapsController._findTopQuoteAndCalculateSavings({}),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('fetchAndSetQuotes', function () {
|
describe('fetchAndSetQuotes', function () {
|
||||||
it('returns null if fetchParams is not provided', async function () {
|
it('returns null if fetchParams is not provided', async function () {
|
||||||
const quotes = await swapsController.fetchAndSetQuotes(undefined)
|
const quotes = await swapsController.fetchAndSetQuotes(undefined)
|
||||||
@ -279,7 +273,7 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
|
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
|
||||||
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||||
|
|
||||||
// Make it so approval is not required
|
// Make it so approval is not required
|
||||||
sandbox
|
sandbox
|
||||||
@ -291,8 +285,8 @@ describe('SwapsController', function () {
|
|||||||
MOCK_FETCH_METADATA,
|
MOCK_FETCH_METADATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepStrictEqual(newQuotes[TEST_AGG_ID], {
|
assert.deepStrictEqual(newQuotes[TEST_AGG_ID_BEST], {
|
||||||
...MOCK_QUOTES[TEST_AGG_ID],
|
...getMockQuotes()[TEST_AGG_ID_BEST],
|
||||||
sourceTokenInfo: undefined,
|
sourceTokenInfo: undefined,
|
||||||
destinationTokenInfo: {
|
destinationTokenInfo: {
|
||||||
symbol: 'FOO',
|
symbol: 'FOO',
|
||||||
@ -301,7 +295,12 @@ describe('SwapsController', function () {
|
|||||||
isBestQuote: true,
|
isBestQuote: true,
|
||||||
// TODO: find a way to calculate these values dynamically
|
// TODO: find a way to calculate these values dynamically
|
||||||
gasEstimate: 2000000,
|
gasEstimate: 2000000,
|
||||||
gasEstimateWithRefund: '8cd8e',
|
gasEstimateWithRefund: 'b8cae',
|
||||||
|
savings: {
|
||||||
|
fee: '0',
|
||||||
|
performance: '6',
|
||||||
|
total: '6',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
@ -311,7 +310,7 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('performs the allowance check', async function () {
|
it('performs the allowance check', async function () {
|
||||||
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||||
|
|
||||||
// Make it so approval is not required
|
// Make it so approval is not required
|
||||||
const allowanceStub = sandbox
|
const allowanceStub = sandbox
|
||||||
@ -358,7 +357,7 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('marks the best quote', async function () {
|
it('marks the best quote', async function () {
|
||||||
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||||
|
|
||||||
// Make it so approval is not required
|
// Make it so approval is not required
|
||||||
sandbox
|
sandbox
|
||||||
@ -370,7 +369,7 @@ describe('SwapsController', function () {
|
|||||||
MOCK_FETCH_METADATA,
|
MOCK_FETCH_METADATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.strictEqual(topAggId, TEST_AGG_ID)
|
assert.strictEqual(topAggId, TEST_AGG_ID_BEST)
|
||||||
assert.strictEqual(newQuotes[topAggId].isBestQuote, true)
|
assert.strictEqual(newQuotes[topAggId].isBestQuote, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -379,15 +378,15 @@ describe('SwapsController', function () {
|
|||||||
|
|
||||||
// Clone the existing mock quote and increase destination amount
|
// Clone the existing mock quote and increase destination amount
|
||||||
const bestQuote = {
|
const bestQuote = {
|
||||||
...MOCK_QUOTES[TEST_AGG_ID],
|
...getMockQuotes()[TEST_AGG_ID_1],
|
||||||
aggregator: bestAggId,
|
aggregator: bestAggId,
|
||||||
destinationAmount: ethers.BigNumber.from(
|
destinationAmount: ethers.BigNumber.from(
|
||||||
MOCK_QUOTES[TEST_AGG_ID].destinationAmount,
|
getMockQuotes()[TEST_AGG_ID_1].destinationAmount,
|
||||||
)
|
)
|
||||||
.add(1)
|
.add((100e18).toString())
|
||||||
.toString(),
|
.toString(),
|
||||||
}
|
}
|
||||||
const quotes = { ...MOCK_QUOTES, [bestAggId]: bestQuote }
|
const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote }
|
||||||
fetchTradesInfoStub.resolves(quotes)
|
fetchTradesInfoStub.resolves(quotes)
|
||||||
|
|
||||||
// Make it so approval is not required
|
// Make it so approval is not required
|
||||||
@ -405,7 +404,7 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('does not mark as best quote if no conversion rate exists for destination token', async function () {
|
it('does not mark as best quote if no conversion rate exists for destination token', async function () {
|
||||||
fetchTradesInfoStub.resolves(MOCK_QUOTES)
|
fetchTradesInfoStub.resolves(getMockQuotes())
|
||||||
|
|
||||||
// Make it so approval is not required
|
// Make it so approval is not required
|
||||||
sandbox
|
sandbox
|
||||||
@ -762,4 +761,150 @@ describe('SwapsController', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('utils', function () {
|
||||||
|
describe('getMedian', function () {
|
||||||
|
const { getMedian } = utils
|
||||||
|
|
||||||
|
it('calculates median correctly with uneven sample', function () {
|
||||||
|
const values = [3, 2, 6].map((value) => new BigNumber(value))
|
||||||
|
const median = getMedian(values)
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
median.toNumber(), 3,
|
||||||
|
'should have returned correct median',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('calculates median correctly with even sample', function () {
|
||||||
|
const values = [3, 2, 2, 6].map((value) => new BigNumber(value))
|
||||||
|
const median = getMedian(values)
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
median.toNumber(), 2.5,
|
||||||
|
'should have returned correct median',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on empty or non-array sample', function () {
|
||||||
|
assert.throws(
|
||||||
|
() => getMedian([]),
|
||||||
|
'should throw on empty array',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => getMedian(),
|
||||||
|
'should throw on non-array param',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => getMedian({}),
|
||||||
|
'should throw on non-array param',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function getMockQuotes () {
|
||||||
|
return {
|
||||||
|
[TEST_AGG_ID_1]: {
|
||||||
|
'trade': {
|
||||||
|
'from': '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc',
|
||||||
|
'value': '0x0',
|
||||||
|
'gas': '0x61a80', // 4e5
|
||||||
|
'to': '0x881D40237659C251811CEC9c364ef91dC08D300C',
|
||||||
|
},
|
||||||
|
'sourceAmount': '10000000000000000000', // 10e18
|
||||||
|
'destinationAmount': '20000000000000000000', // 20e18
|
||||||
|
'error': null,
|
||||||
|
'sourceToken': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'destinationToken': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'approvalNeeded': null,
|
||||||
|
'maxGas': 600000,
|
||||||
|
'averageGas': 120000,
|
||||||
|
'estimatedRefund': 80000,
|
||||||
|
'fetchTime': 607,
|
||||||
|
'aggregator': TEST_AGG_ID_1,
|
||||||
|
'aggType': 'AGG',
|
||||||
|
'slippage': 2,
|
||||||
|
'sourceTokenInfo': {
|
||||||
|
'address': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'symbol': 'DAI',
|
||||||
|
'decimals': 18,
|
||||||
|
'iconUrl': 'https://foo.bar/logo.png',
|
||||||
|
},
|
||||||
|
'destinationTokenInfo': {
|
||||||
|
'address': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'symbol': 'USDC',
|
||||||
|
'decimals': 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[TEST_AGG_ID_BEST]: {
|
||||||
|
'trade': {
|
||||||
|
'from': '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc',
|
||||||
|
'value': '0x0',
|
||||||
|
'gas': '0x61a80',
|
||||||
|
'to': '0x881D40237659C251811CEC9c364ef91dC08D300C',
|
||||||
|
},
|
||||||
|
'sourceAmount': '10000000000000000000',
|
||||||
|
'destinationAmount': '25000000000000000000', // 25e18
|
||||||
|
'error': null,
|
||||||
|
'sourceToken': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'destinationToken': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'approvalNeeded': null,
|
||||||
|
'maxGas': 1100000,
|
||||||
|
'averageGas': 411000,
|
||||||
|
'estimatedRefund': 343090,
|
||||||
|
'fetchTime': 1003,
|
||||||
|
'aggregator': TEST_AGG_ID_BEST,
|
||||||
|
'aggType': 'AGG',
|
||||||
|
'slippage': 2,
|
||||||
|
'sourceTokenInfo': {
|
||||||
|
'address': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'symbol': 'DAI',
|
||||||
|
'decimals': 18,
|
||||||
|
'iconUrl': 'https://foo.bar/logo.png',
|
||||||
|
},
|
||||||
|
'destinationTokenInfo': {
|
||||||
|
'address': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'symbol': 'USDC',
|
||||||
|
'decimals': 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[TEST_AGG_ID_2]: {
|
||||||
|
'trade': {
|
||||||
|
'from': '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc',
|
||||||
|
'value': '0x0',
|
||||||
|
'gas': '0x61a80',
|
||||||
|
'to': '0x881D40237659C251811CEC9c364ef91dC08D300C',
|
||||||
|
},
|
||||||
|
'sourceAmount': '10000000000000000000',
|
||||||
|
'destinationAmount': '22000000000000000000', // 22e18
|
||||||
|
'error': null,
|
||||||
|
'sourceToken': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'destinationToken': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'approvalNeeded': null,
|
||||||
|
'maxGas': 368000,
|
||||||
|
'averageGas': 197000,
|
||||||
|
'estimatedRefund': 18205,
|
||||||
|
'fetchTime': 1354,
|
||||||
|
'aggregator': TEST_AGG_ID_2,
|
||||||
|
'aggType': 'AGG',
|
||||||
|
'slippage': 2,
|
||||||
|
'sourceTokenInfo': {
|
||||||
|
'address': '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||||
|
'symbol': 'DAI',
|
||||||
|
'decimals': 18,
|
||||||
|
'iconUrl': 'https://foo.bar/logo.png',
|
||||||
|
},
|
||||||
|
'destinationTokenInfo': {
|
||||||
|
'address': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||||
|
'symbol': 'USDC',
|
||||||
|
'decimals': 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user