diff options
Diffstat (limited to 'ui/app/selectors')
-rw-r--r-- | ui/app/selectors/custom-gas.js | 270 | ||||
-rw-r--r-- | ui/app/selectors/tests/custom-gas.test.js | 277 |
2 files changed, 547 insertions, 0 deletions
diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js new file mode 100644 index 000000000..59f240f9c --- /dev/null +++ b/ui/app/selectors/custom-gas.js @@ -0,0 +1,270 @@ +import { pipe, partialRight } from 'ramda' +import { + conversionUtil, + multiplyCurrencies, +} from '../conversion-util' +import { + getCurrentCurrency, +} from '../selectors' +import { + formatCurrency, +} from '../helpers/confirm-transaction/util' +import { + decEthToConvertedCurrency as ethTotalToConvertedCurrency, +} from '../helpers/conversions.util' +import { + formatETHFee, +} from '../helpers/formatters' +import { + calcGasTotal, +} from '../components/send/send.utils' +import { addHexPrefix } from 'ethereumjs-util' + +const selectors = { + formatTimeEstimate, + getAveragePriceEstimateInHexWEI, + getFastPriceEstimateInHexWEI, + getBasicGasEstimateLoadingStatus, + getBasicGasEstimateBlockTime, + getCustomGasErrors, + getCustomGasLimit, + getCustomGasPrice, + getCustomGasTotal, + getDefaultActiveButtonIndex, + getEstimatedGasPrices, + getEstimatedGasTimes, + getGasEstimatesLoadingStatus, + getPriceAndTimeEstimates, + getRenderableBasicEstimateData, + getRenderableEstimateDataForSmallButtonsFromGWEI, + priceEstimateToWei, +} + +module.exports = selectors + +const NUMBER_OF_DECIMALS_SM_BTNS = 5 + +function getCustomGasErrors (state) { + return state.gas.errors +} + +function getCustomGasLimit (state) { + return state.gas.customData.limit +} + +function getCustomGasPrice (state) { + return state.gas.customData.price +} + +function getCustomGasTotal (state) { + return state.gas.customData.total +} + +function getBasicGasEstimateLoadingStatus (state) { + return state.gas.basicEstimateIsLoading +} + +function getGasEstimatesLoadingStatus (state) { + return state.gas.gasEstimatesLoading +} + +function getPriceAndTimeEstimates (state) { + return state.gas.priceAndTimeEstimates +} + +function getEstimatedGasPrices (state) { + return getPriceAndTimeEstimates(state).map(({ gasprice }) => gasprice) +} + +function getEstimatedGasTimes (state) { + return getPriceAndTimeEstimates(state).map(({ expectedTime }) => expectedTime) +} + +function getAveragePriceEstimateInHexWEI (state) { + const averagePriceEstimate = state.gas.basicEstimates.average + return getGasPriceInHexWei(averagePriceEstimate || '0x0') +} + +function getFastPriceEstimateInHexWEI (state) { + const fastPriceEstimate = state.gas.basicEstimates.fast + return getGasPriceInHexWei(fastPriceEstimate || '0x0') +} + +function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { + return gasButtonInfo.findIndex(({ priceInHexWei }) => { + return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice) + }) +} + +function getBasicGasEstimateBlockTime (state) { + return state.gas.basicEstimates.blockTime +} + +function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) { + return conversionUtil(calcGasTotal(gasLimit, estimate), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'GWEI', + numberOfDecimals, + }) +} + +function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) { + return pipe( + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), + partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]), + formatETHFee + )(estimate, gasLimit) +} + + +function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) { + return pipe( + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), + partialRight(basicPriceEstimateToETHTotal, [gasLimit]), + partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), + partialRight(formatCurrency, [convertedCurrency]) + )(estimate, gasLimit, convertedCurrency, conversionRate) +} + +function getTimeEstimateInSeconds (blockWaitEstimate) { + return multiplyCurrencies(blockWaitEstimate, 60, { + toNumericBase: 'dec', + multiplicandBase: 10, + multiplierBase: 10, + numberOfDecimals: 1, + }) +} + +function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) { + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + + if (!minutes && !seconds) { + return '...' + } + + let symbol = '~' + if (greaterThanMax) { + symbol = '< ' + } else if (lessThanMin) { + symbol = '> ' + } + + const formattedMin = `${minutes ? minutes + ' min' : ''}` + const formattedSec = `${seconds ? seconds + ' sec' : ''}` + const formattedCombined = formattedMin && formattedSec + ? `${symbol}${formattedMin} ${formattedSec}` + : symbol + [formattedMin, formattedSec].find(t => t) + + return formattedCombined +} + +function getRenderableTimeEstimate (blockWaitEstimate) { + return pipe( + getTimeEstimateInSeconds, + formatTimeEstimate + )(blockWaitEstimate) +} + +function priceEstimateToWei (priceEstimate) { + return conversionUtil(priceEstimate, { + fromNumericBase: 'hex', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + numberOfDecimals: 9, + }) +} + +function getGasPriceInHexWei (price) { + return pipe( + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), + priceEstimateToWei, + addHexPrefix + )(price) +} + +function getRenderableBasicEstimateData (state) { + if (getBasicGasEstimateLoadingStatus(state)) { + return [] + } + const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state) + const conversionRate = state.metamask.conversionRate + const currentCurrency = getCurrentCurrency(state) + const { + gas: { + basicEstimates: { + safeLow, + fast, + fastest, + safeLowWait, + fastestWait, + fastWait, + }, + }, + } = state + + return [ + { + labelKey: 'fastest', + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(fastest, gasLimit), + timeEstimate: fastestWait && getRenderableTimeEstimate(fastestWait), + priceInHexWei: getGasPriceInHexWei(fastest), + }, + { + labelKey: 'fast', + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(fast, gasLimit), + timeEstimate: fastWait && getRenderableTimeEstimate(fastWait), + priceInHexWei: getGasPriceInHexWei(fast), + }, + { + labelKey: 'slow', + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(safeLow, gasLimit), + timeEstimate: safeLowWait && getRenderableTimeEstimate(safeLowWait), + priceInHexWei: getGasPriceInHexWei(safeLow), + }, + ] +} + +function getRenderableEstimateDataForSmallButtonsFromGWEI (state) { + if (getBasicGasEstimateLoadingStatus(state)) { + return [] + } + const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state) + const conversionRate = state.metamask.conversionRate + const currentCurrency = getCurrentCurrency(state) + const { + gas: { + basicEstimates: { + safeLow, + fast, + fastest, + }, + }, + } = state + + return [ + { + labelKey: 'fastest', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(fastest, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), + priceInHexWei: getGasPriceInHexWei(fastest, true), + }, + { + labelKey: 'fast', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), + priceInHexWei: getGasPriceInHexWei(fast, true), + }, + { + labelKey: 'slow', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), + priceInHexWei: getGasPriceInHexWei(safeLow, true), + }, + ] +} diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js new file mode 100644 index 000000000..ebc300160 --- /dev/null +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -0,0 +1,277 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +const { + getCustomGasErrors, + getCustomGasLimit, + getCustomGasPrice, + getCustomGasTotal, + getEstimatedGasPrices, + getEstimatedGasTimes, + getPriceAndTimeEstimates, + getRenderableBasicEstimateData, + getRenderableEstimateDataForSmallButtonsFromGWEI, +} = proxyquire('../custom-gas', {}) + +describe('custom-gas selectors', () => { + + describe('getCustomGasPrice()', () => { + it('should return gas.customData.price', () => { + const mockState = { gas: { customData: { price: 'mockPrice' } } } + assert.equal(getCustomGasPrice(mockState), 'mockPrice') + }) + }) + + describe('getCustomGasLimit()', () => { + it('should return gas.customData.limit', () => { + const mockState = { gas: { customData: { limit: 'mockLimit' } } } + assert.equal(getCustomGasLimit(mockState), 'mockLimit') + }) + }) + + describe('getCustomGasTotal()', () => { + it('should return gas.customData.total', () => { + const mockState = { gas: { customData: { total: 'mockTotal' } } } + assert.equal(getCustomGasTotal(mockState), 'mockTotal') + }) + }) + + describe('getCustomGasErrors()', () => { + it('should return gas.errors', () => { + const mockState = { gas: { errors: 'mockErrors' } } + assert.equal(getCustomGasErrors(mockState), 'mockErrors') + }) + }) + + describe('getPriceAndTimeEstimates', () => { + it('should return price and time estimates', () => { + const mockState = { gas: { priceAndTimeEstimates: 'mockPriceAndTimeEstimates' } } + assert.equal(getPriceAndTimeEstimates(mockState), 'mockPriceAndTimeEstimates') + }) + }) + + describe('getEstimatedGasPrices', () => { + it('should return price and time estimates', () => { + const mockState = { gas: { priceAndTimeEstimates: [ + { gasprice: 12, somethingElse: 20 }, + { gasprice: 22, expectedTime: 30 }, + { gasprice: 32, somethingElse: 40 }, + ] } } + assert.deepEqual(getEstimatedGasPrices(mockState), [12, 22, 32]) + }) + }) + + describe('getEstimatedGasTimes', () => { + it('should return price and time estimates', () => { + const mockState = { gas: { priceAndTimeEstimates: [ + { somethingElse: 12, expectedTime: 20 }, + { gasPrice: 22, expectedTime: 30 }, + { somethingElse: 32, expectedTime: 40 }, + ] } } + assert.deepEqual(getEstimatedGasTimes(mockState), [20, 30, 40]) + }) + }) + + describe('getRenderableBasicEstimateData()', () => { + const tests = [ + { + expectedResult: [ + { + labelKey: 'fastest', + feeInPrimaryCurrency: '$0.05', + feeInSecondaryCurrency: '0.00021 ETH', + timeEstimate: '~30 sec', + priceInHexWei: '0x2540be400', + }, + { + labelKey: 'fast', + feeInPrimaryCurrency: '$0.03', + feeInSecondaryCurrency: '0.000105 ETH', + timeEstimate: '~3 min 18 sec', + priceInHexWei: '0x12a05f200', + }, + { + labelKey: 'slow', + feeInPrimaryCurrency: '$0.01', + feeInSecondaryCurrency: '0.0000525 ETH', + timeEstimate: '~6 min 36 sec', + priceInHexWei: '0x9502f900', + }, + ], + mockState: { + metamask: { + conversionRate: 255.71, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 2.5, + safeLowWait: 6.6, + fast: 5, + fastWait: 3.3, + fastest: 10, + fastestWait: 0.5, + }, + }, + }, + }, + { + expectedResult: [ + { + labelKey: 'fastest', + feeInPrimaryCurrency: '$1.07', + feeInSecondaryCurrency: '0.00042 ETH', + timeEstimate: '~1 min', + priceInHexWei: '0x4a817c800', + }, + { + labelKey: 'fast', + feeInPrimaryCurrency: '$0.54', + feeInSecondaryCurrency: '0.00021 ETH', + timeEstimate: '~6 min 36 sec', + priceInHexWei: '0x2540be400', + }, + { + labelKey: 'slow', + feeInPrimaryCurrency: '$0.27', + feeInSecondaryCurrency: '0.000105 ETH', + timeEstimate: '~13 min 12 sec', + priceInHexWei: '0x12a05f200', + }, + ], + mockState: { + metamask: { + conversionRate: 2557.1, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 5, + safeLowWait: 13.2, + fast: 10, + fastWait: 6.6, + fastest: 20, + fastestWait: 1.0, + }, + }, + }, + }, + ] + it('should return renderable data about basic estimates', () => { + tests.forEach(test => { + assert.deepEqual( + getRenderableBasicEstimateData(test.mockState), + test.expectedResult + ) + }) + }) + + }) + + describe('getRenderableEstimateDataForSmallButtonsFromGWEI()', () => { + const tests = [ + { + expectedResult: [ + { + feeInSecondaryCurrency: '$0.54', + feeInPrimaryCurrency: '0.0021 ETH', + labelKey: 'fastest', + priceInHexWei: '0x174876e800', + }, + { + feeInSecondaryCurrency: '$0.27', + feeInPrimaryCurrency: '0.00105 ETH', + labelKey: 'fast', + priceInHexWei: '0xba43b7400', + }, + { + feeInSecondaryCurrency: '$0.13', + feeInPrimaryCurrency: '0.00052 ETH', + labelKey: 'slow', + priceInHexWei: '0x5d21dba00', + }, + ], + mockState: { + metamask: { + conversionRate: 255.71, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 25, + safeLowWait: 6.6, + fast: 50, + fastWait: 3.3, + fastest: 100, + fastestWait: 0.5, + }, + }, + }, + }, + { + expectedResult: [ + { + feeInSecondaryCurrency: '$10.74', + feeInPrimaryCurrency: '0.0042 ETH', + labelKey: 'fastest', + priceInHexWei: '0x2e90edd000', + }, + { + feeInSecondaryCurrency: '$5.37', + feeInPrimaryCurrency: '0.0021 ETH', + labelKey: 'fast', + priceInHexWei: '0x174876e800', + }, + { + feeInSecondaryCurrency: '$2.68', + feeInPrimaryCurrency: '0.00105 ETH', + labelKey: 'slow', + priceInHexWei: '0xba43b7400', + }, + ], + mockState: { + metamask: { + conversionRate: 2557.1, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 50, + safeLowWait: 13.2, + fast: 100, + fastWait: 6.6, + fastest: 200, + fastestWait: 1.0, + }, + }, + }, + }, + ] + it('should return renderable data about basic estimates appropriate for buttons with less info', () => { + tests.forEach(test => { + assert.deepEqual( + getRenderableEstimateDataForSmallButtonsFromGWEI(test.mockState), + test.expectedResult + ) + }) + }) + + }) + +}) |