Merge pull request #5704 from MetaMask/new-gas-customize-feature-branch-d
Gas customization featuresfeature/default_network_editable
commit
d1996509de
File diff suppressed because one or more lines are too long
@ -0,0 +1,144 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import classnames from 'classnames' |
||||||
|
import Loading from '../../../loading-screen' |
||||||
|
import GasPriceChart from '../../gas-price-chart' |
||||||
|
import debounce from 'lodash.debounce' |
||||||
|
|
||||||
|
export default class AdvancedTabContent extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
updateCustomGasPrice: PropTypes.func, |
||||||
|
updateCustomGasLimit: PropTypes.func, |
||||||
|
customGasPrice: PropTypes.number, |
||||||
|
customGasLimit: PropTypes.number, |
||||||
|
gasEstimatesLoading: PropTypes.bool, |
||||||
|
millisecondsRemaining: PropTypes.number, |
||||||
|
totalFee: PropTypes.string, |
||||||
|
timeRemaining: PropTypes.string, |
||||||
|
gasChartProps: PropTypes.object, |
||||||
|
insufficientBalance: PropTypes.bool, |
||||||
|
} |
||||||
|
|
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
this.debouncedGasLimitReset = debounce((dVal) => { |
||||||
|
if (dVal < 21000) { |
||||||
|
props.updateCustomGasLimit(21000) |
||||||
|
} |
||||||
|
}, 1000, { trailing: true }) |
||||||
|
this.onChangeGasLimit = (val) => { |
||||||
|
props.updateCustomGasLimit(val) |
||||||
|
this.debouncedGasLimitReset(val) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
gasInput (value, onChange, min, insufficientBalance, showGWEI) { |
||||||
|
return ( |
||||||
|
<div className="advanced-tab__gas-edit-row__input-wrapper"> |
||||||
|
<input |
||||||
|
className={classnames('advanced-tab__gas-edit-row__input', { |
||||||
|
'advanced-tab__gas-edit-row__input--error': insufficientBalance, |
||||||
|
})} |
||||||
|
type="number" |
||||||
|
value={value} |
||||||
|
min={min} |
||||||
|
onChange={event => onChange(Number(event.target.value))} |
||||||
|
/> |
||||||
|
<div className={classnames('advanced-tab__gas-edit-row__input-arrows', { |
||||||
|
'advanced-tab__gas-edit-row__input-arrows--error': insufficientBalance, |
||||||
|
})}> |
||||||
|
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value + 1)}><i className="fa fa-sm fa-angle-up" /></div> |
||||||
|
<div className="advanced-tab__gas-edit-row__input-arrows__i-wrap" onClick={() => onChange(value - 1)}><i className="fa fa-sm fa-angle-down" /></div> |
||||||
|
</div> |
||||||
|
{insufficientBalance && <div className="advanced-tab__gas-edit-row__insufficient-balance"> |
||||||
|
Insufficient Balance |
||||||
|
</div>} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
infoButton (onClick) { |
||||||
|
return <i className="fa fa-info-circle" onClick={onClick} /> |
||||||
|
} |
||||||
|
|
||||||
|
renderDataSummary (totalFee, timeRemaining) { |
||||||
|
return ( |
||||||
|
<div className="advanced-tab__transaction-data-summary"> |
||||||
|
<div className="advanced-tab__transaction-data-summary__titles"> |
||||||
|
<span>{ this.context.t('newTransactionFee') }</span> |
||||||
|
<span>~{ this.context.t('transactionTime') }</span> |
||||||
|
</div> |
||||||
|
<div className="advanced-tab__transaction-data-summary__container"> |
||||||
|
<div className="advanced-tab__transaction-data-summary__fee"> |
||||||
|
{totalFee} |
||||||
|
</div> |
||||||
|
<div className="time-remaining">{timeRemaining}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderGasEditRow (labelKey, ...gasInputArgs) { |
||||||
|
return ( |
||||||
|
<div className="advanced-tab__gas-edit-row"> |
||||||
|
<div className="advanced-tab__gas-edit-row__label"> |
||||||
|
{ this.context.t(labelKey) } |
||||||
|
{ this.infoButton(() => {}) } |
||||||
|
</div> |
||||||
|
{ this.gasInput(...gasInputArgs) } |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) { |
||||||
|
return ( |
||||||
|
<div className="advanced-tab__gas-edit-rows"> |
||||||
|
{ this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, true) } |
||||||
|
{ this.renderGasEditRow('gasLimit', customGasLimit, this.onChangeGasLimit, customGasLimit, insufficientBalance) } |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
updateCustomGasPrice, |
||||||
|
updateCustomGasLimit, |
||||||
|
timeRemaining, |
||||||
|
customGasPrice, |
||||||
|
customGasLimit, |
||||||
|
insufficientBalance, |
||||||
|
totalFee, |
||||||
|
gasChartProps, |
||||||
|
gasEstimatesLoading, |
||||||
|
} = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="advanced-tab"> |
||||||
|
{ this.renderDataSummary(totalFee, timeRemaining) } |
||||||
|
<div className="advanced-tab__fee-chart"> |
||||||
|
{ this.renderGasEditRows( |
||||||
|
customGasPrice, |
||||||
|
updateCustomGasPrice, |
||||||
|
customGasLimit, |
||||||
|
updateCustomGasLimit, |
||||||
|
insufficientBalance |
||||||
|
) } |
||||||
|
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div> |
||||||
|
{!gasEstimatesLoading |
||||||
|
? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} /> |
||||||
|
: <Loading /> |
||||||
|
} |
||||||
|
<div className="advanced-tab__fee-chart__speed-buttons"> |
||||||
|
<span>Slower</span> |
||||||
|
<span>Faster</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './advanced-tab-content.component' |
@ -0,0 +1,191 @@ |
|||||||
|
@import './time-remaining/index'; |
||||||
|
|
||||||
|
.advanced-tab { |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
|
||||||
|
&__transaction-data-summary, |
||||||
|
&__fee-chart-title { |
||||||
|
padding-left: 24px; |
||||||
|
padding-right: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
&__transaction-data-summary { |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
color: $mid-gray; |
||||||
|
margin-top: 12px; |
||||||
|
padding-left: 18px; |
||||||
|
padding-right: 18px; |
||||||
|
|
||||||
|
&__titles, |
||||||
|
&__container { |
||||||
|
display: flex; |
||||||
|
flex-flow: row; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 12px; |
||||||
|
color: #888EA3; |
||||||
|
} |
||||||
|
|
||||||
|
&__container { |
||||||
|
font-size: 16px; |
||||||
|
margin-top: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
&__fee { |
||||||
|
font-size: 16px; |
||||||
|
color: #313A5E; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__fee-chart { |
||||||
|
margin-top: 8px; |
||||||
|
height: 265px; |
||||||
|
background: #F8F9FB; |
||||||
|
border-bottom: 1px solid #d2d8dd; |
||||||
|
border-top: 1px solid #d2d8dd; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
&__title { |
||||||
|
font-size: 12px; |
||||||
|
color: #313A5E; |
||||||
|
margin-left: 22px; |
||||||
|
} |
||||||
|
|
||||||
|
&__speed-buttons { |
||||||
|
position: absolute; |
||||||
|
bottom: 13px; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
padding-left: 20px; |
||||||
|
padding-right: 19px; |
||||||
|
width: 100%; |
||||||
|
font-size: 10px; |
||||||
|
color: #888EA3; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__slider-container { |
||||||
|
padding-left: 27px; |
||||||
|
padding-right: 27px; |
||||||
|
} |
||||||
|
|
||||||
|
&__gas-edit-rows { |
||||||
|
height: 73px; |
||||||
|
display: flex; |
||||||
|
flex-flow: row; |
||||||
|
justify-content: space-between; |
||||||
|
margin-left: 20px; |
||||||
|
margin-right: 10px; |
||||||
|
margin-top: 9px; |
||||||
|
} |
||||||
|
|
||||||
|
&__gas-edit-row { |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
|
||||||
|
&__label { |
||||||
|
color: #313B5E; |
||||||
|
font-size: 14px; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
|
||||||
|
.fa-info-circle { |
||||||
|
color: $silver; |
||||||
|
margin-left: 10px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.fa-info-circle:hover { |
||||||
|
color: $mid-gray; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__insufficient-balance { |
||||||
|
font-size: 12px; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
&__input-wrapper { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
&__input { |
||||||
|
border: 1px solid $dusty-gray; |
||||||
|
border-radius: 4px; |
||||||
|
color: $mid-gray; |
||||||
|
font-size: 16px; |
||||||
|
height: 24px; |
||||||
|
width: 155px; |
||||||
|
padding-left: 8px; |
||||||
|
padding-top: 2px; |
||||||
|
margin-top: 7px; |
||||||
|
} |
||||||
|
|
||||||
|
&__input--error { |
||||||
|
border: 1px solid $red; |
||||||
|
} |
||||||
|
|
||||||
|
&__input-arrows { |
||||||
|
position: absolute; |
||||||
|
top: 7px; |
||||||
|
right: 0px; |
||||||
|
width: 17px; |
||||||
|
height: 24px; |
||||||
|
border: 1px solid #dadada; |
||||||
|
border-top-right-radius: 4px; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
color: #9b9b9b; |
||||||
|
font-size: .8em; |
||||||
|
border-bottom-right-radius: 4px; |
||||||
|
cursor: pointer; |
||||||
|
|
||||||
|
&__i-wrap { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
&__i-wrap:hover { |
||||||
|
background: #4EADE7; |
||||||
|
color: $white; |
||||||
|
} |
||||||
|
|
||||||
|
i:hover { |
||||||
|
background: #4EADE7; |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
font-size: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__input-arrows--error { |
||||||
|
border: 1px solid $red; |
||||||
|
} |
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button { |
||||||
|
-webkit-appearance: none; |
||||||
|
-moz-appearance: none; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
input[type="number"]:hover::-webkit-inner-spin-button { |
||||||
|
-webkit-appearance: none; |
||||||
|
-moz-appearance: none; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
&__gwei-symbol { |
||||||
|
position: absolute; |
||||||
|
top: 8px; |
||||||
|
right: 10px; |
||||||
|
color: $dusty-gray; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,273 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import shallow from '../../../../../../lib/shallow-with-context' |
||||||
|
import sinon from 'sinon' |
||||||
|
import AdvancedTabContent from '../advanced-tab-content.component.js' |
||||||
|
|
||||||
|
import GasPriceChart from '../../../gas-price-chart' |
||||||
|
import Loading from '../../../../loading-screen' |
||||||
|
|
||||||
|
const propsMethodSpies = { |
||||||
|
updateCustomGasPrice: sinon.spy(), |
||||||
|
updateCustomGasLimit: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow') |
||||||
|
sinon.spy(AdvancedTabContent.prototype, 'gasInput') |
||||||
|
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows') |
||||||
|
sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary') |
||||||
|
|
||||||
|
describe('AdvancedTabContent Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<AdvancedTabContent |
||||||
|
updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice} |
||||||
|
updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit} |
||||||
|
customGasPrice={11} |
||||||
|
customGasLimit={23456} |
||||||
|
timeRemaining={21500} |
||||||
|
totalFee={'$0.25'} |
||||||
|
insufficientBalance={false} |
||||||
|
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
propsMethodSpies.updateCustomGasPrice.resetHistory() |
||||||
|
propsMethodSpies.updateCustomGasLimit.resetHistory() |
||||||
|
AdvancedTabContent.prototype.renderGasEditRow.resetHistory() |
||||||
|
AdvancedTabContent.prototype.gasInput.resetHistory() |
||||||
|
AdvancedTabContent.prototype.renderGasEditRows.resetHistory() |
||||||
|
AdvancedTabContent.prototype.renderDataSummary.resetHistory() |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render()', () => { |
||||||
|
it('should render the advanced-tab root node', () => { |
||||||
|
assert(wrapper.hasClass('advanced-tab')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the expected four children of the advanced-tab div', () => { |
||||||
|
const advancedTabChildren = wrapper.children() |
||||||
|
assert.equal(advancedTabChildren.length, 2) |
||||||
|
|
||||||
|
assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) |
||||||
|
assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) |
||||||
|
|
||||||
|
const feeChartDiv = advancedTabChildren.at(1) |
||||||
|
|
||||||
|
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) |
||||||
|
assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title')) |
||||||
|
assert(feeChartDiv.childAt(2).is(GasPriceChart)) |
||||||
|
assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => { |
||||||
|
wrapper.setProps({ gasEstimatesLoading: true }) |
||||||
|
const advancedTabChildren = wrapper.children() |
||||||
|
assert.equal(advancedTabChildren.length, 2) |
||||||
|
|
||||||
|
assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) |
||||||
|
assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) |
||||||
|
|
||||||
|
const feeChartDiv = advancedTabChildren.at(1) |
||||||
|
|
||||||
|
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) |
||||||
|
assert(feeChartDiv.childAt(1).hasClass('advanced-tab__fee-chart__title')) |
||||||
|
assert(feeChartDiv.childAt(2).is(Loading)) |
||||||
|
assert(feeChartDiv.childAt(3).hasClass('advanced-tab__fee-chart__speed-buttons')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call renderDataSummary with the expected params', () => { |
||||||
|
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) |
||||||
|
const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args |
||||||
|
assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call renderGasEditRows with the expected params', () => { |
||||||
|
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) |
||||||
|
const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args |
||||||
|
assert.deepEqual(renderGasEditRowArgs, [ |
||||||
|
11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, false, |
||||||
|
]) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderDataSummary()', () => { |
||||||
|
let dataSummary |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the transaction-data-summary root node', () => { |
||||||
|
assert(dataSummary.hasClass('advanced-tab__transaction-data-summary')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render titles of the data', () => { |
||||||
|
const titlesNode = dataSummary.children().at(0) |
||||||
|
assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles')) |
||||||
|
assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee') |
||||||
|
assert.equal(titlesNode.children().at(1).text(), '~transactionTime') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the data', () => { |
||||||
|
const dataNode = dataSummary.children().at(1) |
||||||
|
assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container')) |
||||||
|
assert.equal(dataNode.children().at(0).text(), 'mockTotalFee') |
||||||
|
assert(dataNode.children().at(1).hasClass('time-remaining')) |
||||||
|
assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderGasEditRow()', () => { |
||||||
|
let gasEditRow |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
AdvancedTabContent.prototype.gasInput.resetHistory() |
||||||
|
gasEditRow = shallow(wrapper.instance().renderGasEditRow( |
||||||
|
'mockLabelKey', 'argA', 'argB' |
||||||
|
)) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the gas-edit-row root node', () => { |
||||||
|
assert(gasEditRow.hasClass('advanced-tab__gas-edit-row')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a label and an input', () => { |
||||||
|
const gasEditRowChildren = gasEditRow.children() |
||||||
|
assert.equal(gasEditRowChildren.length, 2) |
||||||
|
assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label')) |
||||||
|
assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the label key and info button', () => { |
||||||
|
const gasRowLabelChildren = gasEditRow.children().at(0).children() |
||||||
|
assert.equal(gasRowLabelChildren.length, 2) |
||||||
|
assert(gasRowLabelChildren.at(0), 'mockLabelKey') |
||||||
|
assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call this.gasInput with the correct args', () => { |
||||||
|
const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args |
||||||
|
assert.deepEqual(gasInputSpyArgs[0], [ 'argA', 'argB' ]) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderGasEditRows()', () => { |
||||||
|
let gasEditRows |
||||||
|
let tempOnChangeGasLimit |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit |
||||||
|
wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit' |
||||||
|
AdvancedTabContent.prototype.renderGasEditRow.resetHistory() |
||||||
|
gasEditRows = shallow(wrapper.instance().renderGasEditRows( |
||||||
|
'mockGasPrice', |
||||||
|
() => 'mockUpdateCustomGasPriceReturn', |
||||||
|
'mockGasLimit', |
||||||
|
() => 'mockUpdateCustomGasLimitReturn', |
||||||
|
false |
||||||
|
)) |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the gas-edit-rows root node', () => { |
||||||
|
assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render two rows', () => { |
||||||
|
const gasEditRowsChildren = gasEditRows.children() |
||||||
|
assert.equal(gasEditRowsChildren.length, 2) |
||||||
|
assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row')) |
||||||
|
assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call this.renderGasEditRow twice, with the expected args', () => { |
||||||
|
const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args |
||||||
|
assert.equal(renderGasEditRowSpyArgs.length, 2) |
||||||
|
assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ |
||||||
|
'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, true, |
||||||
|
].map(String)) |
||||||
|
assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ |
||||||
|
'gasLimit', 'mockGasLimit', () => 'mockOnChangeGasLimit', 'mockGasLimit', false, |
||||||
|
].map(String)) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('infoButton()', () => { |
||||||
|
let infoButton |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
AdvancedTabContent.prototype.renderGasEditRow.resetHistory() |
||||||
|
infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the i element', () => { |
||||||
|
assert(infoButton.hasClass('fa-info-circle')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass the onClick argument to the i tag onClick prop', () => { |
||||||
|
assert(infoButton.props().onClick(), 'mockOnClickReturn') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('gasInput()', () => { |
||||||
|
let gasInput |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
AdvancedTabContent.prototype.renderGasEditRow.resetHistory() |
||||||
|
gasInput = shallow(wrapper.instance().gasInput( |
||||||
|
321, |
||||||
|
value => value + 7, |
||||||
|
0, |
||||||
|
false, |
||||||
|
8 |
||||||
|
)) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the input-wrapper root node', () => { |
||||||
|
assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render two children, including an input', () => { |
||||||
|
assert.equal(gasInput.children().length, 2) |
||||||
|
assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass the correct value min and precision props to the input', () => { |
||||||
|
const inputProps = gasInput.find('input').props() |
||||||
|
assert.equal(inputProps.min, 0) |
||||||
|
assert.equal(inputProps.value, 321) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call the passed onChange method with the value of the input onChange event', () => { |
||||||
|
const inputOnChange = gasInput.find('input').props().onChange |
||||||
|
assert.equal(inputOnChange({ target: { value: 8} }), 15) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should have two input arrows', () => { |
||||||
|
const upArrow = gasInput.find('.fa-angle-up') |
||||||
|
assert.equal(upArrow.length, 1) |
||||||
|
const downArrow = gasInput.find('.fa-angle-down') |
||||||
|
assert.equal(downArrow.length, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call onChange with the value incremented decremented when its onchange method is called', () => { |
||||||
|
gasInput = shallow(wrapper.instance().gasInput( |
||||||
|
321, |
||||||
|
value => value + 7, |
||||||
|
0, |
||||||
|
8, |
||||||
|
false |
||||||
|
)) |
||||||
|
const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0) |
||||||
|
assert.equal(upArrow.props().onClick(), 329) |
||||||
|
const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) |
||||||
|
assert.equal(downArrow.props().onClick(), 327) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './time-remaining.component' |
@ -0,0 +1,17 @@ |
|||||||
|
.time-remaining { |
||||||
|
color: #313A5E; |
||||||
|
font-size: 16px; |
||||||
|
|
||||||
|
.minutes-num, .seconds-num { |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.seconds-num { |
||||||
|
margin-left: 7px; |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.minutes-label, .seconds-label { |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import shallow from '../../../../../../../lib/shallow-with-context' |
||||||
|
import TimeRemaining from '../time-remaining.component.js' |
||||||
|
|
||||||
|
describe('TimeRemaining Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<TimeRemaining |
||||||
|
milliseconds={495000} |
||||||
|
/>) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render()', () => { |
||||||
|
it('should render the time-remaining root node', () => { |
||||||
|
assert(wrapper.hasClass('time-remaining')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render minutes and seconds numbers and labels', () => { |
||||||
|
const timeRemainingChildren = wrapper.children() |
||||||
|
assert.equal(timeRemainingChildren.length, 4) |
||||||
|
assert.equal(timeRemainingChildren.at(0).text(), 8) |
||||||
|
assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand') |
||||||
|
assert.equal(timeRemainingChildren.at(2).text(), 15) |
||||||
|
assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
@ -0,0 +1,33 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import { getTimeBreakdown } from './time-remaining.utils' |
||||||
|
|
||||||
|
export default class TimeRemaining extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
milliseconds: PropTypes.number, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
milliseconds, |
||||||
|
} = this.props |
||||||
|
|
||||||
|
const { |
||||||
|
minutes, |
||||||
|
seconds, |
||||||
|
} = getTimeBreakdown(milliseconds) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="time-remaining"> |
||||||
|
<span className="minutes-num">{minutes}</span> |
||||||
|
<span className="minutes-label">{this.context.t('minutesShorthand')}</span> |
||||||
|
<span className="seconds-num">{seconds}</span> |
||||||
|
<span className="seconds-label">{this.context.t('secondsShorthand')}</span> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
function getTimeBreakdown (milliseconds) { |
||||||
|
return { |
||||||
|
hours: Math.floor(milliseconds / 3600000), |
||||||
|
minutes: Math.floor((milliseconds % 3600000) / 60000), |
||||||
|
seconds: Math.floor((milliseconds % 60000) / 1000), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
getTimeBreakdown, |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import Loading from '../../../loading-screen' |
||||||
|
import GasPriceButtonGroup from '../../gas-price-button-group' |
||||||
|
|
||||||
|
export default class BasicTabContent extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
gasPriceButtonGroupProps: PropTypes.object, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { gasPriceButtonGroupProps } = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="basic-tab-content"> |
||||||
|
<div className="basic-tab-content__title">Estimated Processing Times</div> |
||||||
|
<div className="basic-tab-content__blurb">Select a higher gas fee to accelerate the processing of your transaction.*</div> |
||||||
|
{!gasPriceButtonGroupProps.loading |
||||||
|
? <GasPriceButtonGroup |
||||||
|
className="gas-price-button-group--alt" |
||||||
|
showCheck={true} |
||||||
|
{...gasPriceButtonGroupProps} |
||||||
|
/> |
||||||
|
: <Loading /> |
||||||
|
} |
||||||
|
<div className="basic-tab-content__footer-blurb">* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed.</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './basic-tab-content.component' |
@ -0,0 +1,28 @@ |
|||||||
|
.basic-tab-content { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: flex-start; |
||||||
|
padding-left: 21px; |
||||||
|
height: 324px; |
||||||
|
background: #F5F7F8; |
||||||
|
border-bottom: 1px solid #d2d8dd; |
||||||
|
|
||||||
|
&__title { |
||||||
|
margin-top: 19px; |
||||||
|
font-size: 16px; |
||||||
|
color: $black; |
||||||
|
} |
||||||
|
|
||||||
|
&__blurb { |
||||||
|
font-size: 12px; |
||||||
|
color: $black; |
||||||
|
margin-top: 5px; |
||||||
|
margin-bottom: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
&__footer-blurb { |
||||||
|
font-size: 12px; |
||||||
|
color: #979797; |
||||||
|
margin-top: 15px; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import { shallow } from 'enzyme' |
||||||
|
import BasicTabContent from '../basic-tab-content.component' |
||||||
|
|
||||||
|
import GasPriceButtonGroup from '../../../gas-price-button-group/' |
||||||
|
import Loading from '../../../../loading-screen' |
||||||
|
|
||||||
|
const mockGasPriceButtonGroupProps = { |
||||||
|
buttonDataLoading: false, |
||||||
|
className: 'gas-price-button-group', |
||||||
|
gasButtonInfo: [ |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.52', |
||||||
|
feeInSecondaryCurrency: '0.0048 ETH', |
||||||
|
timeEstimate: '~ 1 min 0 sec', |
||||||
|
priceInHexWei: '0xa1b2c3f', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.39', |
||||||
|
feeInSecondaryCurrency: '0.004 ETH', |
||||||
|
timeEstimate: '~ 1 min 30 sec', |
||||||
|
priceInHexWei: '0xa1b2c39', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.30', |
||||||
|
feeInSecondaryCurrency: '0.00354 ETH', |
||||||
|
timeEstimate: '~ 2 min 1 sec', |
||||||
|
priceInHexWei: '0xa1b2c30', |
||||||
|
}, |
||||||
|
], |
||||||
|
handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice), |
||||||
|
noButtonActiveByDefault: true, |
||||||
|
showCheck: true, |
||||||
|
} |
||||||
|
|
||||||
|
describe('BasicTabContent Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<BasicTabContent |
||||||
|
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} |
||||||
|
/>) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render', () => { |
||||||
|
it('should have a title', () => { |
||||||
|
assert(wrapper.find('.basic-tab-content').childAt(0).hasClass('basic-tab-content__title')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a GasPriceButtonGroup compenent', () => { |
||||||
|
assert.equal(wrapper.find(GasPriceButtonGroup).length, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass correct props to GasPriceButtonGroup', () => { |
||||||
|
const { |
||||||
|
buttonDataLoading, |
||||||
|
className, |
||||||
|
gasButtonInfo, |
||||||
|
handleGasPriceSelection, |
||||||
|
noButtonActiveByDefault, |
||||||
|
showCheck, |
||||||
|
} = wrapper.find(GasPriceButtonGroup).props() |
||||||
|
assert.equal(wrapper.find(GasPriceButtonGroup).length, 1) |
||||||
|
assert.equal(buttonDataLoading, mockGasPriceButtonGroupProps.buttonDataLoading) |
||||||
|
assert.equal(className, mockGasPriceButtonGroupProps.className) |
||||||
|
assert.equal(noButtonActiveByDefault, mockGasPriceButtonGroupProps.noButtonActiveByDefault) |
||||||
|
assert.equal(showCheck, mockGasPriceButtonGroupProps.showCheck) |
||||||
|
assert.deepEqual(gasButtonInfo, mockGasPriceButtonGroupProps.gasButtonInfo) |
||||||
|
assert.equal(JSON.stringify(handleGasPriceSelection), JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection)) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => { |
||||||
|
wrapper.setProps({ |
||||||
|
gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, loading: true }, |
||||||
|
}) |
||||||
|
|
||||||
|
assert.equal(wrapper.find(GasPriceButtonGroup).length, 0) |
||||||
|
assert.equal(wrapper.find(Loading).length, 1) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,178 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import PageContainer from '../../page-container' |
||||||
|
import { Tabs, Tab } from '../../tabs' |
||||||
|
import AdvancedTabContent from './advanced-tab-content' |
||||||
|
import BasicTabContent from './basic-tab-content' |
||||||
|
|
||||||
|
export default class GasModalPageContainer extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
hideModal: PropTypes.func, |
||||||
|
hideBasic: PropTypes.bool, |
||||||
|
updateCustomGasPrice: PropTypes.func, |
||||||
|
updateCustomGasLimit: PropTypes.func, |
||||||
|
customGasPrice: PropTypes.number, |
||||||
|
customGasLimit: PropTypes.number, |
||||||
|
fetchBasicGasAndTimeEstimates: PropTypes.func, |
||||||
|
fetchGasEstimates: PropTypes.func, |
||||||
|
gasPriceButtonGroupProps: PropTypes.object, |
||||||
|
infoRowProps: PropTypes.shape({ |
||||||
|
originalTotalFiat: PropTypes.string, |
||||||
|
originalTotalEth: PropTypes.string, |
||||||
|
newTotalFiat: PropTypes.string, |
||||||
|
newTotalEth: PropTypes.string, |
||||||
|
}), |
||||||
|
onSubmit: PropTypes.func, |
||||||
|
customModalGasPriceInHex: PropTypes.string, |
||||||
|
customModalGasLimitInHex: PropTypes.string, |
||||||
|
cancelAndClose: PropTypes.func, |
||||||
|
transactionFee: PropTypes.string, |
||||||
|
blockTime: PropTypes.oneOfType([ |
||||||
|
PropTypes.string, |
||||||
|
PropTypes.number, |
||||||
|
]), |
||||||
|
} |
||||||
|
|
||||||
|
state = {} |
||||||
|
|
||||||
|
componentDidMount () { |
||||||
|
const promise = this.props.hideBasic |
||||||
|
? Promise.resolve(this.props.blockTime) |
||||||
|
: this.props.fetchBasicGasAndTimeEstimates() |
||||||
|
.then(basicEstimates => basicEstimates.blockTime) |
||||||
|
|
||||||
|
promise |
||||||
|
.then(blockTime => { |
||||||
|
this.props.fetchGasEstimates(blockTime) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
renderBasicTabContent (gasPriceButtonGroupProps) { |
||||||
|
return ( |
||||||
|
<BasicTabContent |
||||||
|
gasPriceButtonGroupProps={gasPriceButtonGroupProps} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderAdvancedTabContent ({ |
||||||
|
convertThenUpdateCustomGasPrice, |
||||||
|
convertThenUpdateCustomGasLimit, |
||||||
|
customGasPrice, |
||||||
|
customGasLimit, |
||||||
|
newTotalFiat, |
||||||
|
gasChartProps, |
||||||
|
currentTimeEstimate, |
||||||
|
insufficientBalance, |
||||||
|
gasEstimatesLoading, |
||||||
|
}) { |
||||||
|
const { transactionFee } = this.props |
||||||
|
return ( |
||||||
|
<AdvancedTabContent |
||||||
|
updateCustomGasPrice={convertThenUpdateCustomGasPrice} |
||||||
|
updateCustomGasLimit={convertThenUpdateCustomGasLimit} |
||||||
|
customGasPrice={customGasPrice} |
||||||
|
customGasLimit={customGasLimit} |
||||||
|
timeRemaining={currentTimeEstimate} |
||||||
|
transactionFee={transactionFee} |
||||||
|
totalFee={newTotalFiat} |
||||||
|
gasChartProps={gasChartProps} |
||||||
|
insufficientBalance={insufficientBalance} |
||||||
|
gasEstimatesLoading={gasEstimatesLoading} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) { |
||||||
|
return ( |
||||||
|
<div className="gas-modal-content__info-row-wrapper"> |
||||||
|
<div className="gas-modal-content__info-row"> |
||||||
|
<div className="gas-modal-content__info-row__send-info"> |
||||||
|
<span className="gas-modal-content__info-row__send-info__label">{this.context.t('sendAmount')}</span> |
||||||
|
<span className="gas-modal-content__info-row__send-info__value">{sendAmount}</span> |
||||||
|
</div> |
||||||
|
<div className="gas-modal-content__info-row__transaction-info"> |
||||||
|
<span className={'gas-modal-content__info-row__transaction-info__label'}>{this.context.t('transactionFee')}</span> |
||||||
|
<span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span> |
||||||
|
</div> |
||||||
|
<div className="gas-modal-content__info-row__total-info"> |
||||||
|
<span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span> |
||||||
|
<span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span> |
||||||
|
</div> |
||||||
|
<div className="gas-modal-content__info-row__fiat-total-info"> |
||||||
|
<span className="gas-modal-content__info-row__fiat-total-info__value">{newTotalFiat}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderTabs ({ |
||||||
|
originalTotalFiat, |
||||||
|
originalTotalEth, |
||||||
|
newTotalFiat, |
||||||
|
newTotalEth, |
||||||
|
sendAmount, |
||||||
|
transactionFee, |
||||||
|
}, |
||||||
|
{ |
||||||
|
gasPriceButtonGroupProps, |
||||||
|
hideBasic, |
||||||
|
...advancedTabProps |
||||||
|
}) { |
||||||
|
let tabsToRender = [ |
||||||
|
{ name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) }, |
||||||
|
{ name: 'advanced', content: this.renderAdvancedTabContent(advancedTabProps) }, |
||||||
|
] |
||||||
|
|
||||||
|
if (hideBasic) { |
||||||
|
tabsToRender = tabsToRender.slice(1) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Tabs> |
||||||
|
{tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}> |
||||||
|
<div className="gas-modal-content"> |
||||||
|
{ content } |
||||||
|
{ this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } |
||||||
|
</div> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
</Tabs> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
cancelAndClose, |
||||||
|
infoRowProps, |
||||||
|
onSubmit, |
||||||
|
customModalGasPriceInHex, |
||||||
|
customModalGasLimitInHex, |
||||||
|
...tabProps |
||||||
|
} = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="gas-modal-page-container"> |
||||||
|
<PageContainer |
||||||
|
title={this.context.t('customGas')} |
||||||
|
subtitle={this.context.t('customGasSubTitle')} |
||||||
|
tabsComponent={this.renderTabs(infoRowProps, tabProps)} |
||||||
|
disabled={tabProps.insufficientBalance} |
||||||
|
onCancel={() => cancelAndClose()} |
||||||
|
onClose={() => cancelAndClose()} |
||||||
|
onSubmit={() => { |
||||||
|
onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) |
||||||
|
}} |
||||||
|
submitText={this.context.t('save')} |
||||||
|
headerCloseText={'Close'} |
||||||
|
hideCancel={true} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,283 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import { pipe, partialRight } from 'ramda' |
||||||
|
import GasModalPageContainer from './gas-modal-page-container.component' |
||||||
|
import { |
||||||
|
hideModal, |
||||||
|
setGasLimit, |
||||||
|
setGasPrice, |
||||||
|
createSpeedUpTransaction, |
||||||
|
hideSidebar, |
||||||
|
} from '../../../actions' |
||||||
|
import { |
||||||
|
setCustomGasPrice, |
||||||
|
setCustomGasLimit, |
||||||
|
resetCustomData, |
||||||
|
setCustomTimeEstimate, |
||||||
|
fetchGasEstimates, |
||||||
|
fetchBasicGasAndTimeEstimates, |
||||||
|
} from '../../../ducks/gas.duck' |
||||||
|
import { |
||||||
|
hideGasButtonGroup, |
||||||
|
} from '../../../ducks/send.duck' |
||||||
|
import { |
||||||
|
updateGasAndCalculate, |
||||||
|
} from '../../../ducks/confirm-transaction.duck' |
||||||
|
import { |
||||||
|
getCurrentCurrency, |
||||||
|
conversionRateSelector as getConversionRate, |
||||||
|
getSelectedToken, |
||||||
|
getCurrentEthBalance, |
||||||
|
} from '../../../selectors.js' |
||||||
|
import { |
||||||
|
formatTimeEstimate, |
||||||
|
getFastPriceEstimateInHexWEI, |
||||||
|
getBasicGasEstimateLoadingStatus, |
||||||
|
getGasEstimatesLoadingStatus, |
||||||
|
getCustomGasLimit, |
||||||
|
getCustomGasPrice, |
||||||
|
getDefaultActiveButtonIndex, |
||||||
|
getEstimatedGasPrices, |
||||||
|
getEstimatedGasTimes, |
||||||
|
getRenderableBasicEstimateData, |
||||||
|
getBasicGasEstimateBlockTime, |
||||||
|
} from '../../../selectors/custom-gas' |
||||||
|
import { |
||||||
|
submittedPendingTransactionsSelector, |
||||||
|
} from '../../../selectors/transactions' |
||||||
|
import { |
||||||
|
formatCurrency, |
||||||
|
} from '../../../helpers/confirm-transaction/util' |
||||||
|
import { |
||||||
|
addHexWEIsToDec, |
||||||
|
decEthToConvertedCurrency as ethTotalToConvertedCurrency, |
||||||
|
decGWEIToHexWEI, |
||||||
|
hexWEIToDecGWEI, |
||||||
|
} from '../../../helpers/conversions.util' |
||||||
|
import { |
||||||
|
formatETHFee, |
||||||
|
} from '../../../helpers/formatters' |
||||||
|
import { |
||||||
|
calcGasTotal, |
||||||
|
isBalanceSufficient, |
||||||
|
} from '../../send/send.utils' |
||||||
|
import { addHexPrefix } from 'ethereumjs-util' |
||||||
|
import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' |
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => { |
||||||
|
const { transaction = {} } = ownProps |
||||||
|
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) |
||||||
|
const gasEstimatesLoading = getGasEstimatesLoadingStatus(state) |
||||||
|
|
||||||
|
const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id) |
||||||
|
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice |
||||||
|
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit |
||||||
|
const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) |
||||||
|
|
||||||
|
const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) |
||||||
|
|
||||||
|
const gasButtonInfo = getRenderableBasicEstimateData(state) |
||||||
|
|
||||||
|
const currentCurrency = getCurrentCurrency(state) |
||||||
|
const conversionRate = getConversionRate(state) |
||||||
|
|
||||||
|
const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate) |
||||||
|
|
||||||
|
const hideBasic = state.appState.modal.modalState.props.hideBasic |
||||||
|
|
||||||
|
const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) |
||||||
|
|
||||||
|
const gasPrices = getEstimatedGasPrices(state) |
||||||
|
const estimatedTimes = getEstimatedGasTimes(state) |
||||||
|
const balance = getCurrentEthBalance(state) |
||||||
|
|
||||||
|
const insufficientBalance = !isBalanceSufficient({ |
||||||
|
amount: value, |
||||||
|
gasTotal, |
||||||
|
balance, |
||||||
|
conversionRate, |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
hideBasic, |
||||||
|
isConfirm: isConfirm(state), |
||||||
|
customModalGasPriceInHex, |
||||||
|
customModalGasLimitInHex, |
||||||
|
customGasPrice, |
||||||
|
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), |
||||||
|
newTotalFiat, |
||||||
|
currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), |
||||||
|
blockTime: getBasicGasEstimateBlockTime(state), |
||||||
|
gasPriceButtonGroupProps: { |
||||||
|
buttonDataLoading, |
||||||
|
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), |
||||||
|
gasButtonInfo, |
||||||
|
}, |
||||||
|
gasChartProps: { |
||||||
|
currentPrice: customGasPrice, |
||||||
|
gasPrices, |
||||||
|
estimatedTimes, |
||||||
|
gasPricesMax: gasPrices[gasPrices.length - 1], |
||||||
|
estimatedTimesMax: estimatedTimes[0], |
||||||
|
}, |
||||||
|
infoRowProps: { |
||||||
|
originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), |
||||||
|
originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), |
||||||
|
newTotalFiat, |
||||||
|
newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), |
||||||
|
transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), |
||||||
|
sendAmount: addHexWEIsToRenderableEth(value, '0x0'), |
||||||
|
}, |
||||||
|
isSpeedUp: transaction.status === 'submitted', |
||||||
|
txId: transaction.id, |
||||||
|
insufficientBalance, |
||||||
|
gasEstimatesLoading, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => { |
||||||
|
const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice))) |
||||||
|
|
||||||
|
return { |
||||||
|
cancelAndClose: () => { |
||||||
|
dispatch(resetCustomData()) |
||||||
|
dispatch(hideModal()) |
||||||
|
}, |
||||||
|
hideModal: () => dispatch(hideModal()), |
||||||
|
updateCustomGasPrice, |
||||||
|
convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), |
||||||
|
convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), |
||||||
|
setGasData: (newLimit, newPrice) => { |
||||||
|
dispatch(setGasLimit(newLimit)) |
||||||
|
dispatch(setGasPrice(newPrice)) |
||||||
|
}, |
||||||
|
updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { |
||||||
|
updateCustomGasPrice(gasPrice) |
||||||
|
dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16)))) |
||||||
|
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) |
||||||
|
}, |
||||||
|
createSpeedUpTransaction: (txId, gasPrice) => { |
||||||
|
return dispatch(createSpeedUpTransaction(txId, gasPrice)) |
||||||
|
}, |
||||||
|
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), |
||||||
|
setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), |
||||||
|
hideSidebar: () => dispatch(hideSidebar()), |
||||||
|
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), |
||||||
|
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||||
|
const { gasPriceButtonGroupProps, isConfirm, isSpeedUp, txId } = stateProps |
||||||
|
const { |
||||||
|
updateCustomGasPrice: dispatchUpdateCustomGasPrice, |
||||||
|
hideGasButtonGroup: dispatchHideGasButtonGroup, |
||||||
|
setGasData: dispatchSetGasData, |
||||||
|
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, |
||||||
|
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, |
||||||
|
hideSidebar: dispatchHideSidebar, |
||||||
|
cancelAndClose: dispatchCancelAndClose, |
||||||
|
hideModal: dispatchHideModal, |
||||||
|
...otherDispatchProps |
||||||
|
} = dispatchProps |
||||||
|
|
||||||
|
return { |
||||||
|
...stateProps, |
||||||
|
...otherDispatchProps, |
||||||
|
...ownProps, |
||||||
|
onSubmit: (gasLimit, gasPrice) => { |
||||||
|
if (isConfirm) { |
||||||
|
dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice) |
||||||
|
dispatchHideModal() |
||||||
|
} else if (isSpeedUp) { |
||||||
|
dispatchCreateSpeedUpTransaction(txId, gasPrice) |
||||||
|
dispatchHideSidebar() |
||||||
|
dispatchCancelAndClose() |
||||||
|
} else { |
||||||
|
dispatchSetGasData(gasLimit, gasPrice) |
||||||
|
dispatchHideGasButtonGroup() |
||||||
|
dispatchCancelAndClose() |
||||||
|
} |
||||||
|
}, |
||||||
|
gasPriceButtonGroupProps: { |
||||||
|
...gasPriceButtonGroupProps, |
||||||
|
handleGasPriceSelection: dispatchUpdateCustomGasPrice, |
||||||
|
}, |
||||||
|
cancelAndClose: () => { |
||||||
|
dispatchCancelAndClose() |
||||||
|
if (isSpeedUp) { |
||||||
|
dispatchHideSidebar() |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer) |
||||||
|
|
||||||
|
function isConfirm (state) { |
||||||
|
return Boolean(Object.keys(state.confirmTransaction.txData).length) |
||||||
|
} |
||||||
|
|
||||||
|
function calcCustomGasPrice (customGasPriceInHex) { |
||||||
|
return Number(hexWEIToDecGWEI(customGasPriceInHex)) |
||||||
|
} |
||||||
|
|
||||||
|
function calcCustomGasLimit (customGasLimitInHex) { |
||||||
|
return parseInt(customGasLimitInHex, 16) |
||||||
|
} |
||||||
|
|
||||||
|
function getTxParams (state, transactionId) { |
||||||
|
const { confirmTransaction: { txData }, metamask: { send } } = state |
||||||
|
const pendingTransactions = submittedPendingTransactionsSelector(state) |
||||||
|
const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId) |
||||||
|
const { txParams: pendingTxParams } = pendingTransaction || {} |
||||||
|
return txData.txParams || pendingTxParams || { |
||||||
|
from: send.from, |
||||||
|
gas: send.gasLimit, |
||||||
|
gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), |
||||||
|
to: send.to, |
||||||
|
value: getSelectedToken(state) ? '0x0' : send.amount, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { |
||||||
|
return pipe( |
||||||
|
addHexWEIsToDec, |
||||||
|
formatETHFee |
||||||
|
)(aHexWEI, bHexWEI) |
||||||
|
} |
||||||
|
|
||||||
|
function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) { |
||||||
|
return pipe( |
||||||
|
addHexWEIsToDec, |
||||||
|
partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), |
||||||
|
partialRight(formatCurrency, [convertedCurrency]), |
||||||
|
)(aHexWEI, bHexWEI) |
||||||
|
} |
||||||
|
|
||||||
|
function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { |
||||||
|
const minGasPrice = gasPrices[0] |
||||||
|
const maxGasPrice = gasPrices[gasPrices.length - 1] |
||||||
|
let priceForEstimation = currentGasPrice |
||||||
|
if (currentGasPrice < minGasPrice) { |
||||||
|
priceForEstimation = minGasPrice |
||||||
|
} else if (currentGasPrice > maxGasPrice) { |
||||||
|
priceForEstimation = maxGasPrice |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
closestLowerValueIndex, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue, |
||||||
|
closestLowerValue, |
||||||
|
} = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) |
||||||
|
|
||||||
|
const newTimeEstimate = extrapolateY({ |
||||||
|
higherY: estimatedTimes[closestHigherValueIndex], |
||||||
|
lowerY: estimatedTimes[closestLowerValueIndex], |
||||||
|
higherX: closestHigherValue, |
||||||
|
lowerX: closestLowerValue, |
||||||
|
xForExtrapolation: priceForEstimation, |
||||||
|
}) |
||||||
|
|
||||||
|
return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './gas-modal-page-container.container' |
@ -0,0 +1,148 @@ |
|||||||
|
@import './advanced-tab-content/index'; |
||||||
|
@import './basic-tab-content/index'; |
||||||
|
|
||||||
|
.gas-modal-page-container { |
||||||
|
.page-container { |
||||||
|
max-width: 391px; |
||||||
|
min-height: 585px; |
||||||
|
overflow-y: initial; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
max-width: 344px; |
||||||
|
|
||||||
|
&__content { |
||||||
|
display: flex; |
||||||
|
overflow-y: initial; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__header { |
||||||
|
padding: 0px; |
||||||
|
padding-top: 16px; |
||||||
|
|
||||||
|
&--no-padding-bottom { |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__footer { |
||||||
|
header { |
||||||
|
padding-top: 12px; |
||||||
|
padding-bottom: 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__header-close-text { |
||||||
|
font-size: 14px; |
||||||
|
color: #4EADE7; |
||||||
|
position: absolute; |
||||||
|
top: 16px; |
||||||
|
right: 16px; |
||||||
|
cursor: pointer; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
&__title { |
||||||
|
color: $black; |
||||||
|
font-size: 16px; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 16px; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: flex-start; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__subtitle { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
&__tabs { |
||||||
|
margin-top: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
&__tab { |
||||||
|
width: 100%; |
||||||
|
font-size: 14px; |
||||||
|
|
||||||
|
&:last-of-type { |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&--selected { |
||||||
|
color: $curious-blue; |
||||||
|
border-bottom: 2px solid $curious-blue; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.gas-modal-content { |
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
&__basic-tab { |
||||||
|
height: 219px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
&__info-row, &__info-row--fade { |
||||||
|
width: 100%; |
||||||
|
background: $polar; |
||||||
|
padding: 15px 21px; |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
color: $scorpion; |
||||||
|
font-size: 12px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
padding: 4px 21px; |
||||||
|
} |
||||||
|
|
||||||
|
&__send-info, &__transaction-info, &__total-info, &__fiat-total-info { |
||||||
|
display: flex; |
||||||
|
flex-flow: row; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
&__fiat-total-info { |
||||||
|
justify-content: flex-end; |
||||||
|
} |
||||||
|
|
||||||
|
&__total-info { |
||||||
|
&__label { |
||||||
|
font-size: 16px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
font-size: 14px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__value { |
||||||
|
font-size: 16px; |
||||||
|
font-weight: bold; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
font-size: 14px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__transaction-info, &__send-info { |
||||||
|
&__label { |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
&__value { |
||||||
|
font-size: 14px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__info-row--fade { |
||||||
|
background: white; |
||||||
|
color: $dusty-gray; |
||||||
|
border-top: 1px solid $mischka; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,273 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import shallow from '../../../../../lib/shallow-with-context' |
||||||
|
import sinon from 'sinon' |
||||||
|
import GasModalPageContainer from '../gas-modal-page-container.component.js' |
||||||
|
import timeout from '../../../../../lib/test-timeout' |
||||||
|
|
||||||
|
import PageContainer from '../../../page-container' |
||||||
|
|
||||||
|
import { Tab } from '../../../tabs' |
||||||
|
|
||||||
|
const mockBasicGasEstimates = { |
||||||
|
blockTime: 'mockBlockTime', |
||||||
|
} |
||||||
|
|
||||||
|
const propsMethodSpies = { |
||||||
|
cancelAndClose: sinon.spy(), |
||||||
|
onSubmit: sinon.spy(), |
||||||
|
fetchBasicGasAndTimeEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)), |
||||||
|
fetchGasEstimates: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const mockGasPriceButtonGroupProps = { |
||||||
|
buttonDataLoading: false, |
||||||
|
className: 'gas-price-button-group', |
||||||
|
gasButtonInfo: [ |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.52', |
||||||
|
feeInSecondaryCurrency: '0.0048 ETH', |
||||||
|
timeEstimate: '~ 1 min 0 sec', |
||||||
|
priceInHexWei: '0xa1b2c3f', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.39', |
||||||
|
feeInSecondaryCurrency: '0.004 ETH', |
||||||
|
timeEstimate: '~ 1 min 30 sec', |
||||||
|
priceInHexWei: '0xa1b2c39', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.30', |
||||||
|
feeInSecondaryCurrency: '0.00354 ETH', |
||||||
|
timeEstimate: '~ 2 min 1 sec', |
||||||
|
priceInHexWei: '0xa1b2c30', |
||||||
|
}, |
||||||
|
], |
||||||
|
handleGasPriceSelection: 'mockSelectionFunction', |
||||||
|
noButtonActiveByDefault: true, |
||||||
|
showCheck: true, |
||||||
|
newTotalFiat: 'mockNewTotalFiat', |
||||||
|
newTotalEth: 'mockNewTotalEth', |
||||||
|
} |
||||||
|
const mockInfoRowProps = { |
||||||
|
originalTotalFiat: 'mockOriginalTotalFiat', |
||||||
|
originalTotalEth: 'mockOriginalTotalEth', |
||||||
|
newTotalFiat: 'mockNewTotalFiat', |
||||||
|
newTotalEth: 'mockNewTotalEth', |
||||||
|
sendAmount: 'mockSendAmount', |
||||||
|
transactionFee: 'mockTransactionFee', |
||||||
|
} |
||||||
|
|
||||||
|
const GP = GasModalPageContainer.prototype |
||||||
|
describe('GasModalPageContainer Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<GasModalPageContainer |
||||||
|
cancelAndClose={propsMethodSpies.cancelAndClose} |
||||||
|
onSubmit={propsMethodSpies.onSubmit} |
||||||
|
fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates} |
||||||
|
fetchGasEstimates={propsMethodSpies.fetchGasEstimates} |
||||||
|
updateCustomGasPrice={() => 'mockupdateCustomGasPrice'} |
||||||
|
updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} |
||||||
|
customGasPrice={21} |
||||||
|
customGasLimit={54321} |
||||||
|
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} |
||||||
|
infoRowProps={mockInfoRowProps} |
||||||
|
currentTimeEstimate={'1 min 31 sec'} |
||||||
|
customGasPriceInHex={'mockCustomGasPriceInHex'} |
||||||
|
customGasLimitInHex={'mockCustomGasLimitInHex'} |
||||||
|
insufficientBalance={false} |
||||||
|
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
propsMethodSpies.cancelAndClose.resetHistory() |
||||||
|
}) |
||||||
|
|
||||||
|
describe('componentDidMount', () => { |
||||||
|
it('should call props.fetchBasicGasAndTimeEstimates', () => { |
||||||
|
propsMethodSpies.fetchBasicGasAndTimeEstimates.resetHistory() |
||||||
|
assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 0) |
||||||
|
wrapper.instance().componentDidMount() |
||||||
|
assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasAndTimeEstimates', async () => { |
||||||
|
propsMethodSpies.fetchGasEstimates.resetHistory() |
||||||
|
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0) |
||||||
|
wrapper.instance().componentDidMount() |
||||||
|
await timeout(250) |
||||||
|
assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1) |
||||||
|
assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render', () => { |
||||||
|
it('should render a PageContainer compenent', () => { |
||||||
|
assert.equal(wrapper.find(PageContainer).length, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass correct props to PageContainer', () => { |
||||||
|
const { |
||||||
|
title, |
||||||
|
subtitle, |
||||||
|
disabled, |
||||||
|
} = wrapper.find(PageContainer).props() |
||||||
|
assert.equal(title, 'customGas') |
||||||
|
assert.equal(subtitle, 'customGasSubTitle') |
||||||
|
assert.equal(disabled, false) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass the correct onCancel and onClose methods to PageContainer', () => { |
||||||
|
const { |
||||||
|
onCancel, |
||||||
|
onClose, |
||||||
|
} = wrapper.find(PageContainer).props() |
||||||
|
assert.equal(propsMethodSpies.cancelAndClose.callCount, 0) |
||||||
|
onCancel() |
||||||
|
assert.equal(propsMethodSpies.cancelAndClose.callCount, 1) |
||||||
|
onClose() |
||||||
|
assert.equal(propsMethodSpies.cancelAndClose.callCount, 2) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should pass the correct renderTabs property to PageContainer', () => { |
||||||
|
sinon.stub(GP, 'renderTabs').returns('mockTabs') |
||||||
|
const renderTabsWrapperTester = shallow(<GasModalPageContainer |
||||||
|
fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates} |
||||||
|
fetchGasEstimates={propsMethodSpies.fetchGasEstimates} |
||||||
|
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) |
||||||
|
const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props() |
||||||
|
assert.equal(tabsComponent, 'mockTabs') |
||||||
|
GasModalPageContainer.prototype.renderTabs.restore() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderTabs', () => { |
||||||
|
beforeEach(() => { |
||||||
|
sinon.spy(GP, 'renderBasicTabContent') |
||||||
|
sinon.spy(GP, 'renderAdvancedTabContent') |
||||||
|
sinon.spy(GP, 'renderInfoRows') |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
GP.renderBasicTabContent.restore() |
||||||
|
GP.renderAdvancedTabContent.restore() |
||||||
|
GP.renderInfoRows.restore() |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a Tabs component with "Basic" and "Advanced" tabs', () => { |
||||||
|
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { |
||||||
|
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, |
||||||
|
otherProps: 'mockAdvancedTabProps', |
||||||
|
}) |
||||||
|
const renderedTabs = shallow(renderTabsResult) |
||||||
|
assert.equal(renderedTabs.props().className, 'tabs') |
||||||
|
|
||||||
|
const tabs = renderedTabs.find(Tab) |
||||||
|
assert.equal(tabs.length, 2) |
||||||
|
|
||||||
|
assert.equal(tabs.at(0).props().name, 'basic') |
||||||
|
assert.equal(tabs.at(1).props().name, 'advanced') |
||||||
|
|
||||||
|
assert.equal(tabs.at(0).childAt(0).props().className, 'gas-modal-content') |
||||||
|
assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => { |
||||||
|
assert.equal(GP.renderBasicTabContent.callCount, 0) |
||||||
|
assert.equal(GP.renderAdvancedTabContent.callCount, 0) |
||||||
|
|
||||||
|
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) |
||||||
|
|
||||||
|
assert.equal(GP.renderBasicTabContent.callCount, 1) |
||||||
|
assert.equal(GP.renderAdvancedTabContent.callCount, 1) |
||||||
|
|
||||||
|
assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps) |
||||||
|
assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { otherProps: 'mockAdvancedTabProps' }) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call renderInfoRows with the expected props', () => { |
||||||
|
assert.equal(GP.renderInfoRows.callCount, 0) |
||||||
|
|
||||||
|
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) |
||||||
|
|
||||||
|
assert.equal(GP.renderInfoRows.callCount, 2) |
||||||
|
|
||||||
|
assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee']) |
||||||
|
assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee']) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should not render the basic tab if hideBasic is true', () => { |
||||||
|
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { |
||||||
|
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, |
||||||
|
otherProps: 'mockAdvancedTabProps', |
||||||
|
hideBasic: true, |
||||||
|
}) |
||||||
|
|
||||||
|
const renderedTabs = shallow(renderTabsResult) |
||||||
|
const tabs = renderedTabs.find(Tab) |
||||||
|
assert.equal(tabs.length, 1) |
||||||
|
assert.equal(tabs.at(0).props().name, 'advanced') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderBasicTabContent', () => { |
||||||
|
it('should render', () => { |
||||||
|
const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
renderBasicTabContentResult.props.gasPriceButtonGroupProps, |
||||||
|
mockGasPriceButtonGroupProps |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderAdvancedTabContent', () => { |
||||||
|
it('should render with the correct props', () => { |
||||||
|
const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({ |
||||||
|
convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice', |
||||||
|
convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit', |
||||||
|
customGasPrice: 123, |
||||||
|
customGasLimit: 456, |
||||||
|
newTotalFiat: '$0.30', |
||||||
|
currentTimeEstimate: '1 min 31 sec', |
||||||
|
gasEstimatesLoading: 'mockGasEstimatesLoading', |
||||||
|
}) |
||||||
|
const advancedTabContentProps = renderAdvancedTabContentResult.props |
||||||
|
assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') |
||||||
|
assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit') |
||||||
|
assert.equal(advancedTabContentProps.customGasPrice, 123) |
||||||
|
assert.equal(advancedTabContentProps.customGasLimit, 456) |
||||||
|
assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec') |
||||||
|
assert.equal(advancedTabContentProps.totalFee, '$0.30') |
||||||
|
assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderInfoRows', () => { |
||||||
|
it('should render the info rows with the passed data', () => { |
||||||
|
const baseClassName = 'gas-modal-content__info-row' |
||||||
|
const renderedInfoRowsContainer = shallow(wrapper.instance().renderInfoRows( |
||||||
|
'mockNewTotalFiat', |
||||||
|
' mockNewTotalEth', |
||||||
|
' mockSendAmount', |
||||||
|
' mockTransactionFee' |
||||||
|
)) |
||||||
|
|
||||||
|
assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName)) |
||||||
|
|
||||||
|
const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children() |
||||||
|
assert.equal(renderedInfoRows.length, 4) |
||||||
|
assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`)) |
||||||
|
assert(renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`)) |
||||||
|
assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`)) |
||||||
|
assert(renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`)) |
||||||
|
|
||||||
|
assert.equal(renderedInfoRows.at(0).text(), 'sendAmount mockSendAmount') |
||||||
|
assert.equal(renderedInfoRows.at(1).text(), 'transactionFee mockTransactionFee') |
||||||
|
assert.equal(renderedInfoRows.at(2).text(), 'newTotal mockNewTotalEth') |
||||||
|
assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat') |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,360 @@ |
|||||||
|
import assert from 'assert' |
||||||
|
import proxyquire from 'proxyquire' |
||||||
|
import sinon from 'sinon' |
||||||
|
|
||||||
|
let mapStateToProps |
||||||
|
let mapDispatchToProps |
||||||
|
let mergeProps |
||||||
|
|
||||||
|
const actionSpies = { |
||||||
|
hideModal: sinon.spy(), |
||||||
|
setGasLimit: sinon.spy(), |
||||||
|
setGasPrice: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const gasActionSpies = { |
||||||
|
setCustomGasPrice: sinon.spy(), |
||||||
|
setCustomGasLimit: sinon.spy(), |
||||||
|
resetCustomData: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const confirmTransactionActionSpies = { |
||||||
|
updateGasAndCalculate: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const sendActionSpies = { |
||||||
|
hideGasButtonGroup: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
proxyquire('../gas-modal-page-container.container.js', { |
||||||
|
'react-redux': { |
||||||
|
connect: (ms, md, mp) => { |
||||||
|
mapStateToProps = ms |
||||||
|
mapDispatchToProps = md |
||||||
|
mergeProps = mp |
||||||
|
return () => ({}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
'../../../selectors/custom-gas': { |
||||||
|
getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, |
||||||
|
getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`, |
||||||
|
getDefaultActiveButtonIndex: (a, b) => a + b, |
||||||
|
}, |
||||||
|
'../../../actions': actionSpies, |
||||||
|
'../../../ducks/gas.duck': gasActionSpies, |
||||||
|
'../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies, |
||||||
|
'../../../ducks/send.duck': sendActionSpies, |
||||||
|
'../../../selectors.js': { |
||||||
|
getCurrentEthBalance: (state) => state.metamask.balance || '0x0', |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
describe('gas-modal-page-container container', () => { |
||||||
|
|
||||||
|
describe('mapStateToProps()', () => { |
||||||
|
it('should map the correct properties to props', () => { |
||||||
|
const baseMockState = { |
||||||
|
appState: { |
||||||
|
modal: { |
||||||
|
modalState: { |
||||||
|
props: { |
||||||
|
hideBasic: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
metamask: { |
||||||
|
send: { |
||||||
|
gasLimit: '16', |
||||||
|
gasPrice: '32', |
||||||
|
amount: '64', |
||||||
|
}, |
||||||
|
currentCurrency: 'abc', |
||||||
|
conversionRate: 50, |
||||||
|
}, |
||||||
|
gas: { |
||||||
|
basicEstimates: { |
||||||
|
blockTime: 12, |
||||||
|
}, |
||||||
|
customData: { |
||||||
|
limit: 'aaaaaaaa', |
||||||
|
price: 'ffffffff', |
||||||
|
}, |
||||||
|
gasEstimatesLoading: false, |
||||||
|
priceAndTimeEstimates: [ |
||||||
|
{ gasprice: 3, expectedTime: 31 }, |
||||||
|
{ gasprice: 4, expectedTime: 62 }, |
||||||
|
{ gasprice: 5, expectedTime: 93 }, |
||||||
|
{ gasprice: 6, expectedTime: 124 }, |
||||||
|
], |
||||||
|
}, |
||||||
|
confirmTransaction: { |
||||||
|
txData: { |
||||||
|
txParams: { |
||||||
|
gas: '0x1600000', |
||||||
|
gasPrice: '0x3200000', |
||||||
|
value: '0x640000000000000', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
const baseExpectedResult = { |
||||||
|
isConfirm: true, |
||||||
|
customGasPrice: 4.294967295, |
||||||
|
customGasLimit: 2863311530, |
||||||
|
currentTimeEstimate: '~1 min 11 sec', |
||||||
|
newTotalFiat: '637.41', |
||||||
|
blockTime: 12, |
||||||
|
customModalGasLimitInHex: 'aaaaaaaa', |
||||||
|
customModalGasPriceInHex: 'ffffffff', |
||||||
|
gasChartProps: { |
||||||
|
'currentPrice': 4.294967295, |
||||||
|
estimatedTimes: ['31', '62', '93', '124'], |
||||||
|
estimatedTimesMax: '31', |
||||||
|
gasPrices: [3, 4, 5, 6], |
||||||
|
gasPricesMax: 6, |
||||||
|
}, |
||||||
|
gasPriceButtonGroupProps: { |
||||||
|
buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', |
||||||
|
defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff', |
||||||
|
gasButtonInfo: 'mockRenderableBasicEstimateData:4', |
||||||
|
}, |
||||||
|
gasEstimatesLoading: false, |
||||||
|
hideBasic: true, |
||||||
|
infoRowProps: { |
||||||
|
originalTotalFiat: '637.41', |
||||||
|
originalTotalEth: '12.748189 ETH', |
||||||
|
newTotalFiat: '637.41', |
||||||
|
newTotalEth: '12.748189 ETH', |
||||||
|
sendAmount: '0.45036 ETH', |
||||||
|
transactionFee: '12.297829 ETH', |
||||||
|
}, |
||||||
|
insufficientBalance: true, |
||||||
|
isSpeedUp: false, |
||||||
|
txId: 34, |
||||||
|
} |
||||||
|
const baseMockOwnProps = { transaction: { id: 34 } } |
||||||
|
const tests = [ |
||||||
|
{ mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps }, |
||||||
|
{ |
||||||
|
mockState: Object.assign({}, baseMockState, { |
||||||
|
metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' }, |
||||||
|
}), |
||||||
|
expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }), |
||||||
|
mockOwnProps: baseMockOwnProps, |
||||||
|
}, |
||||||
|
{ |
||||||
|
mockState: baseMockState, |
||||||
|
mockOwnProps: Object.assign({}, baseMockOwnProps, { |
||||||
|
transaction: { id: 34, status: 'submitted' }, |
||||||
|
}), |
||||||
|
expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }), |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
let result |
||||||
|
tests.forEach(({ mockState, mockOwnProps, expectedResult}) => { |
||||||
|
result = mapStateToProps(mockState, mockOwnProps) |
||||||
|
assert.deepEqual(result, expectedResult) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
describe('mapDispatchToProps()', () => { |
||||||
|
let dispatchSpy |
||||||
|
let mapDispatchToPropsObject |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
dispatchSpy = sinon.spy() |
||||||
|
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
actionSpies.hideModal.resetHistory() |
||||||
|
gasActionSpies.setCustomGasPrice.resetHistory() |
||||||
|
gasActionSpies.setCustomGasLimit.resetHistory() |
||||||
|
}) |
||||||
|
|
||||||
|
describe('hideGasButtonGroup()', () => { |
||||||
|
it('should dispatch a hideGasButtonGroup action', () => { |
||||||
|
mapDispatchToPropsObject.hideGasButtonGroup() |
||||||
|
assert(dispatchSpy.calledOnce) |
||||||
|
assert(sendActionSpies.hideGasButtonGroup.calledOnce) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('cancelAndClose()', () => { |
||||||
|
it('should dispatch a hideModal action', () => { |
||||||
|
mapDispatchToPropsObject.cancelAndClose() |
||||||
|
assert(dispatchSpy.calledTwice) |
||||||
|
assert(actionSpies.hideModal.calledOnce) |
||||||
|
assert(gasActionSpies.resetCustomData.calledOnce) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('updateCustomGasPrice()', () => { |
||||||
|
it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => { |
||||||
|
mapDispatchToPropsObject.updateCustomGasPrice('ffff') |
||||||
|
assert(dispatchSpy.calledOnce) |
||||||
|
assert(gasActionSpies.setCustomGasPrice.calledOnce) |
||||||
|
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('convertThenUpdateCustomGasPrice()', () => { |
||||||
|
it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => { |
||||||
|
mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff') |
||||||
|
assert(dispatchSpy.calledOnce) |
||||||
|
assert(gasActionSpies.setCustomGasPrice.calledOnce) |
||||||
|
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
describe('convertThenUpdateCustomGasLimit()', () => { |
||||||
|
it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => { |
||||||
|
mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16) |
||||||
|
assert(dispatchSpy.calledOnce) |
||||||
|
assert(gasActionSpies.setCustomGasLimit.calledOnce) |
||||||
|
assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setGasData()', () => { |
||||||
|
it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => { |
||||||
|
mapDispatchToPropsObject.setGasData('ffff', 'aaaa') |
||||||
|
assert(dispatchSpy.calledTwice) |
||||||
|
assert(actionSpies.setGasPrice.calledOnce) |
||||||
|
assert(actionSpies.setGasLimit.calledOnce) |
||||||
|
assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff') |
||||||
|
assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('updateConfirmTxGasAndCalculate()', () => { |
||||||
|
it('should dispatch a updateGasAndCalculate action with the correct props', () => { |
||||||
|
mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa') |
||||||
|
assert.equal(dispatchSpy.callCount, 3) |
||||||
|
assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce) |
||||||
|
assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' }) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
describe('mergeProps', () => { |
||||||
|
let stateProps |
||||||
|
let dispatchProps |
||||||
|
let ownProps |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
stateProps = { |
||||||
|
gasPriceButtonGroupProps: { |
||||||
|
someGasPriceButtonGroupProp: 'foo', |
||||||
|
anotherGasPriceButtonGroupProp: 'bar', |
||||||
|
}, |
||||||
|
isConfirm: true, |
||||||
|
someOtherStateProp: 'baz', |
||||||
|
} |
||||||
|
dispatchProps = { |
||||||
|
updateCustomGasPrice: sinon.spy(), |
||||||
|
hideGasButtonGroup: sinon.spy(), |
||||||
|
setGasData: sinon.spy(), |
||||||
|
updateConfirmTxGasAndCalculate: sinon.spy(), |
||||||
|
someOtherDispatchProp: sinon.spy(), |
||||||
|
createSpeedUpTransaction: sinon.spy(), |
||||||
|
hideSidebar: sinon.spy(), |
||||||
|
hideModal: sinon.spy(), |
||||||
|
cancelAndClose: sinon.spy(), |
||||||
|
} |
||||||
|
ownProps = { someOwnProp: 123 } |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
dispatchProps.updateCustomGasPrice.resetHistory() |
||||||
|
dispatchProps.hideGasButtonGroup.resetHistory() |
||||||
|
dispatchProps.setGasData.resetHistory() |
||||||
|
dispatchProps.updateConfirmTxGasAndCalculate.resetHistory() |
||||||
|
dispatchProps.someOtherDispatchProp.resetHistory() |
||||||
|
dispatchProps.createSpeedUpTransaction.resetHistory() |
||||||
|
dispatchProps.hideSidebar.resetHistory() |
||||||
|
dispatchProps.hideModal.resetHistory() |
||||||
|
}) |
||||||
|
it('should return the expected props when isConfirm is true', () => { |
||||||
|
const result = mergeProps(stateProps, dispatchProps, ownProps) |
||||||
|
|
||||||
|
assert.equal(result.isConfirm, true) |
||||||
|
assert.equal(result.someOtherStateProp, 'baz') |
||||||
|
assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo') |
||||||
|
assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar') |
||||||
|
assert.equal(result.someOwnProp, 123) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) |
||||||
|
assert.equal(dispatchProps.setGasData.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideModal.callCount, 0) |
||||||
|
|
||||||
|
result.onSubmit() |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1) |
||||||
|
assert.equal(dispatchProps.setGasData.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideModal.callCount, 1) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) |
||||||
|
result.gasPriceButtonGroupProps.handleGasPriceSelection() |
||||||
|
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) |
||||||
|
result.someOtherDispatchProp() |
||||||
|
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return the expected props when isConfirm is false', () => { |
||||||
|
const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps) |
||||||
|
|
||||||
|
assert.equal(result.isConfirm, false) |
||||||
|
assert.equal(result.someOtherStateProp, 'baz') |
||||||
|
assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo') |
||||||
|
assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar') |
||||||
|
assert.equal(result.someOwnProp, 123) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) |
||||||
|
assert.equal(dispatchProps.setGasData.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) |
||||||
|
assert.equal(dispatchProps.cancelAndClose.callCount, 0) |
||||||
|
|
||||||
|
result.onSubmit('mockNewLimit', 'mockNewPrice') |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) |
||||||
|
assert.equal(dispatchProps.setGasData.callCount, 1) |
||||||
|
assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice']) |
||||||
|
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1) |
||||||
|
assert.equal(dispatchProps.cancelAndClose.callCount, 1) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) |
||||||
|
result.gasPriceButtonGroupProps.handleGasPriceSelection() |
||||||
|
assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) |
||||||
|
result.someOtherDispatchProp() |
||||||
|
assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => { |
||||||
|
const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps) |
||||||
|
|
||||||
|
result.onSubmit() |
||||||
|
|
||||||
|
assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) |
||||||
|
assert.equal(dispatchProps.setGasData.callCount, 0) |
||||||
|
assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) |
||||||
|
assert.equal(dispatchProps.cancelAndClose.callCount, 1) |
||||||
|
|
||||||
|
assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1) |
||||||
|
assert.equal(dispatchProps.hideSidebar.callCount, 1) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
@ -0,0 +1,89 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import ButtonGroup from '../../button-group' |
||||||
|
import Button from '../../button' |
||||||
|
|
||||||
|
const GAS_OBJECT_PROPTYPES_SHAPE = { |
||||||
|
label: PropTypes.string, |
||||||
|
feeInPrimaryCurrency: PropTypes.string, |
||||||
|
feeInSecondaryCurrency: PropTypes.string, |
||||||
|
timeEstimate: PropTypes.string, |
||||||
|
priceInHexWei: PropTypes.string, |
||||||
|
} |
||||||
|
|
||||||
|
export default class GasPriceButtonGroup extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
buttonDataLoading: PropTypes.bool, |
||||||
|
className: PropTypes.string, |
||||||
|
defaultActiveButtonIndex: PropTypes.number, |
||||||
|
gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)), |
||||||
|
handleGasPriceSelection: PropTypes.func, |
||||||
|
newActiveButtonIndex: PropTypes.number, |
||||||
|
noButtonActiveByDefault: PropTypes.bool, |
||||||
|
showCheck: PropTypes.bool, |
||||||
|
} |
||||||
|
|
||||||
|
renderButtonContent ({ |
||||||
|
labelKey, |
||||||
|
feeInPrimaryCurrency, |
||||||
|
feeInSecondaryCurrency, |
||||||
|
timeEstimate, |
||||||
|
}, { |
||||||
|
className, |
||||||
|
showCheck, |
||||||
|
}) { |
||||||
|
return (<div> |
||||||
|
{ labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> } |
||||||
|
{ timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> } |
||||||
|
{ feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> } |
||||||
|
{ feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> } |
||||||
|
{ showCheck && <div className="button-check-wrapper"><i className="fa fa-check fa-sm" /></div> } |
||||||
|
</div>) |
||||||
|
} |
||||||
|
|
||||||
|
renderButton ({ |
||||||
|
priceInHexWei, |
||||||
|
...renderableGasInfo |
||||||
|
}, { |
||||||
|
buttonDataLoading, |
||||||
|
handleGasPriceSelection, |
||||||
|
...buttonContentPropsAndFlags |
||||||
|
}, index) { |
||||||
|
return ( |
||||||
|
<Button |
||||||
|
onClick={() => handleGasPriceSelection(priceInHexWei)} |
||||||
|
key={`gas-price-button-${index}`} |
||||||
|
> |
||||||
|
{this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)} |
||||||
|
</Button> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
gasButtonInfo, |
||||||
|
defaultActiveButtonIndex = 1, |
||||||
|
newActiveButtonIndex, |
||||||
|
noButtonActiveByDefault = false, |
||||||
|
buttonDataLoading, |
||||||
|
...buttonPropsAndFlags |
||||||
|
} = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
!buttonDataLoading |
||||||
|
? <ButtonGroup |
||||||
|
className={buttonPropsAndFlags.className} |
||||||
|
defaultActiveButtonIndex={defaultActiveButtonIndex} |
||||||
|
newActiveButtonIndex={newActiveButtonIndex} |
||||||
|
noButtonActiveByDefault={noButtonActiveByDefault} |
||||||
|
> |
||||||
|
{ gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } |
||||||
|
</ButtonGroup> |
||||||
|
: <div className={`${buttonPropsAndFlags.className}__loading-container`}>{ this.context.t('loading') }</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './gas-price-button-group.component' |
@ -0,0 +1,235 @@ |
|||||||
|
.gas-price-button-group { |
||||||
|
margin-top: 22px; |
||||||
|
display: flex; |
||||||
|
justify-content: space-evenly; |
||||||
|
width: 100%; |
||||||
|
padding-left: 20px; |
||||||
|
padding-right: 20px; |
||||||
|
|
||||||
|
&__primary-currency { |
||||||
|
font-size: 18px; |
||||||
|
height: 20.5px; |
||||||
|
margin-bottom: 7.5px; |
||||||
|
} |
||||||
|
|
||||||
|
&__time-estimate { |
||||||
|
margin-top: 5.5px; |
||||||
|
color: $silver-chalice; |
||||||
|
height: 15.4px; |
||||||
|
} |
||||||
|
|
||||||
|
&__loading-container { |
||||||
|
height: 130px; |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button, .button-group__button--active { |
||||||
|
height: 130px; |
||||||
|
max-width: 108px; |
||||||
|
font-size: 12px; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
display: flex; |
||||||
|
padding-top: 17px; |
||||||
|
border-radius: 4px; |
||||||
|
border: 2px solid $spindle; |
||||||
|
background: $white; |
||||||
|
color: $scorpion; |
||||||
|
|
||||||
|
div { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
&:last-child { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button--active { |
||||||
|
border: 2px solid $curious-blue; |
||||||
|
color: $scorpion; |
||||||
|
|
||||||
|
i { |
||||||
|
&:last-child { |
||||||
|
display: flex; |
||||||
|
color: $curious-blue; |
||||||
|
margin-top: 8px |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.gas-price-button-group--small { |
||||||
|
display: flex; |
||||||
|
justify-content: stretch; |
||||||
|
max-width: 260px; |
||||||
|
|
||||||
|
&__button-fiat-price { |
||||||
|
font-size: 13px; |
||||||
|
} |
||||||
|
|
||||||
|
&__button-label { |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
&__label { |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
&__primary-currency { |
||||||
|
font-size: 12px; |
||||||
|
|
||||||
|
@media screen and (max-width: 575px) { |
||||||
|
font-size: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__secondary-currency { |
||||||
|
font-size: 12px; |
||||||
|
|
||||||
|
@media screen and (max-width: 575px) { |
||||||
|
font-size: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__loading-container { |
||||||
|
height: 78px; |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button, .button-group__button--active { |
||||||
|
height: 78px; |
||||||
|
background: white; |
||||||
|
color: $scorpion; |
||||||
|
padding-top: 9px; |
||||||
|
padding-left: 8.5px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
padding-left: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
div { |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
align-items: flex-start; |
||||||
|
justify-content: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
&:last-child { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button--active { |
||||||
|
color: $white; |
||||||
|
background: $dodger-blue; |
||||||
|
|
||||||
|
i { |
||||||
|
&:last-child { |
||||||
|
display: flex; |
||||||
|
color: $curious-blue; |
||||||
|
margin-top: 10px |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.gas-price-button-group--alt { |
||||||
|
display: flex; |
||||||
|
justify-content: stretch; |
||||||
|
width: 95%; |
||||||
|
|
||||||
|
&__button-fiat-price { |
||||||
|
font-size: 13px; |
||||||
|
} |
||||||
|
|
||||||
|
&__button-label { |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
&__label { |
||||||
|
font-weight: 500; |
||||||
|
font-size: 10px; |
||||||
|
text-transform: capitalize; |
||||||
|
} |
||||||
|
|
||||||
|
&__primary-currency { |
||||||
|
font-size: 11px; |
||||||
|
margin-top: 3px; |
||||||
|
} |
||||||
|
|
||||||
|
&__secondary-currency { |
||||||
|
font-size: 11px; |
||||||
|
} |
||||||
|
|
||||||
|
&__loading-container { |
||||||
|
height: 78px; |
||||||
|
} |
||||||
|
|
||||||
|
&__time-estimate { |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 500; |
||||||
|
margin-top: 4px; |
||||||
|
color: $black; |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button, .button-group__button--active { |
||||||
|
height: 78px; |
||||||
|
background: white; |
||||||
|
color: #2A4055; |
||||||
|
width: 108px; |
||||||
|
height: 97px; |
||||||
|
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.151579); |
||||||
|
border-radius: 6px; |
||||||
|
border: none; |
||||||
|
|
||||||
|
div { |
||||||
|
display: flex; |
||||||
|
flex-flow: column;; |
||||||
|
align-items: flex-start; |
||||||
|
justify-content: flex-start; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.button-check-wrapper { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
&:first-child { |
||||||
|
margin-right: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
&:last-child { |
||||||
|
margin-left: 6px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.button-group__button--active { |
||||||
|
background: #F7FCFF; |
||||||
|
border: 2px solid #2C8BDC; |
||||||
|
|
||||||
|
.button-check-wrapper { |
||||||
|
height: 16px; |
||||||
|
width: 16px; |
||||||
|
border-radius: 8px; |
||||||
|
position: absolute; |
||||||
|
top: -11px; |
||||||
|
right: -10px; |
||||||
|
background: #D5ECFA; |
||||||
|
display: flex; |
||||||
|
flex-flow: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
i { |
||||||
|
display: flex; |
||||||
|
color: $curious-blue; |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,233 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import shallow from '../../../../../lib/shallow-with-context' |
||||||
|
import sinon from 'sinon' |
||||||
|
import GasPriceButtonGroup from '../gas-price-button-group.component' |
||||||
|
|
||||||
|
import ButtonGroup from '../../../button-group/' |
||||||
|
|
||||||
|
const mockGasPriceButtonGroupProps = { |
||||||
|
buttonDataLoading: false, |
||||||
|
className: 'gas-price-button-group', |
||||||
|
gasButtonInfo: [ |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.52', |
||||||
|
feeInSecondaryCurrency: '0.0048 ETH', |
||||||
|
timeEstimate: '~ 1 min 0 sec', |
||||||
|
priceInHexWei: '0xa1b2c3f', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.39', |
||||||
|
feeInSecondaryCurrency: '0.004 ETH', |
||||||
|
timeEstimate: '~ 1 min 30 sec', |
||||||
|
priceInHexWei: '0xa1b2c39', |
||||||
|
}, |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency: '$0.30', |
||||||
|
feeInSecondaryCurrency: '0.00354 ETH', |
||||||
|
timeEstimate: '~ 2 min 1 sec', |
||||||
|
priceInHexWei: '0xa1b2c30', |
||||||
|
}, |
||||||
|
], |
||||||
|
handleGasPriceSelection: sinon.spy(), |
||||||
|
noButtonActiveByDefault: true, |
||||||
|
defaultActiveButtonIndex: 2, |
||||||
|
showCheck: true, |
||||||
|
} |
||||||
|
|
||||||
|
const mockButtonPropsAndFlags = Object.assign({}, { |
||||||
|
className: mockGasPriceButtonGroupProps.className, |
||||||
|
handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection, |
||||||
|
showCheck: mockGasPriceButtonGroupProps.showCheck, |
||||||
|
}) |
||||||
|
|
||||||
|
sinon.spy(GasPriceButtonGroup.prototype, 'renderButton') |
||||||
|
sinon.spy(GasPriceButtonGroup.prototype, 'renderButtonContent') |
||||||
|
|
||||||
|
describe('GasPriceButtonGroup Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<GasPriceButtonGroup |
||||||
|
{...mockGasPriceButtonGroupProps} |
||||||
|
/>) |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
GasPriceButtonGroup.prototype.renderButton.resetHistory() |
||||||
|
GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() |
||||||
|
mockGasPriceButtonGroupProps.handleGasPriceSelection.resetHistory() |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render', () => { |
||||||
|
it('should render a ButtonGroup', () => { |
||||||
|
assert(wrapper.is(ButtonGroup)) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the correct props on the ButtonGroup', () => { |
||||||
|
const { |
||||||
|
className, |
||||||
|
defaultActiveButtonIndex, |
||||||
|
noButtonActiveByDefault, |
||||||
|
} = wrapper.props() |
||||||
|
assert.equal(className, 'gas-price-button-group') |
||||||
|
assert.equal(defaultActiveButtonIndex, 2) |
||||||
|
assert.equal(noButtonActiveByDefault, true) |
||||||
|
}) |
||||||
|
|
||||||
|
function renderButtonArgsTest (i, mockButtonPropsAndFlags) { |
||||||
|
assert.deepEqual( |
||||||
|
GasPriceButtonGroup.prototype.renderButton.getCall(i).args, |
||||||
|
[ |
||||||
|
Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[i]), |
||||||
|
mockButtonPropsAndFlags, |
||||||
|
i, |
||||||
|
] |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
it('should call this.renderButton 3 times, with the correct args', () => { |
||||||
|
assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3) |
||||||
|
renderButtonArgsTest(0, mockButtonPropsAndFlags) |
||||||
|
renderButtonArgsTest(1, mockButtonPropsAndFlags) |
||||||
|
renderButtonArgsTest(2, mockButtonPropsAndFlags) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should show loading if buttonDataLoading', () => { |
||||||
|
wrapper.setProps({ buttonDataLoading: true }) |
||||||
|
assert(wrapper.is('div')) |
||||||
|
assert(wrapper.hasClass('gas-price-button-group__loading-container')) |
||||||
|
assert.equal(wrapper.text(), 'loading') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderButton', () => { |
||||||
|
let wrappedRenderButtonResult |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() |
||||||
|
const renderButtonResult = GasPriceButtonGroup.prototype.renderButton( |
||||||
|
Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]), |
||||||
|
mockButtonPropsAndFlags |
||||||
|
) |
||||||
|
wrappedRenderButtonResult = shallow(renderButtonResult) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a button', () => { |
||||||
|
assert.equal(wrappedRenderButtonResult.type(), 'button') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call the correct method when clicked', () => { |
||||||
|
assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 0) |
||||||
|
wrappedRenderButtonResult.props().onClick() |
||||||
|
assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 1) |
||||||
|
assert.deepEqual( |
||||||
|
mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args, |
||||||
|
[mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei] |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call this.renderButtonContent with the correct args', () => { |
||||||
|
assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 1) |
||||||
|
const { |
||||||
|
feeInPrimaryCurrency, |
||||||
|
feeInSecondaryCurrency, |
||||||
|
timeEstimate, |
||||||
|
} = mockGasPriceButtonGroupProps.gasButtonInfo[0] |
||||||
|
const { |
||||||
|
showCheck, |
||||||
|
className, |
||||||
|
} = mockGasPriceButtonGroupProps |
||||||
|
assert.deepEqual( |
||||||
|
GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args, |
||||||
|
[ |
||||||
|
{ |
||||||
|
feeInPrimaryCurrency, |
||||||
|
feeInSecondaryCurrency, |
||||||
|
timeEstimate, |
||||||
|
}, |
||||||
|
{ |
||||||
|
showCheck, |
||||||
|
className, |
||||||
|
}, |
||||||
|
] |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderButtonContent', () => { |
||||||
|
it('should render a label if passed a labelKey', () => { |
||||||
|
const renderButtonContentResult = wrapper.instance().renderButtonContent({ |
||||||
|
labelKey: 'mockLabelKey', |
||||||
|
}, { |
||||||
|
className: 'someClass', |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => { |
||||||
|
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ |
||||||
|
feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', |
||||||
|
}, { |
||||||
|
className: 'someClass', |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.find('.someClass__primary-currency').text(), 'mockFeeInPrimaryCurrency') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => { |
||||||
|
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ |
||||||
|
feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', |
||||||
|
}, { |
||||||
|
className: 'someClass', |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.find('.someClass__secondary-currency').text(), 'mockFeeInSecondaryCurrency') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a timeEstimate if passed a timeEstimate', () => { |
||||||
|
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ |
||||||
|
timeEstimate: 'mockTimeEstimate', |
||||||
|
}, { |
||||||
|
className: 'someClass', |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.find('.someClass__time-estimate').text(), 'mockTimeEstimate') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render a check if showCheck is true', () => { |
||||||
|
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, { |
||||||
|
className: 'someClass', |
||||||
|
showCheck: true, |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.find('.fa-check').length, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render all elements if all args passed', () => { |
||||||
|
const renderButtonContentResult = wrapper.instance().renderButtonContent({ |
||||||
|
labelKey: 'mockLabel', |
||||||
|
feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', |
||||||
|
feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', |
||||||
|
timeEstimate: 'mockTimeEstimate', |
||||||
|
}, { |
||||||
|
className: 'someClass', |
||||||
|
showCheck: true, |
||||||
|
}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.children().length, 5) |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
it('should render no elements if all args passed', () => { |
||||||
|
const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {}) |
||||||
|
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) |
||||||
|
assert.equal(wrappedRenderButtonContentResult.children().length, 0) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,108 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import * as d3 from 'd3' |
||||||
|
import { |
||||||
|
generateChart, |
||||||
|
getCoordinateData, |
||||||
|
handleChartUpdate, |
||||||
|
hideDataUI, |
||||||
|
setTickPosition, |
||||||
|
handleMouseMove, |
||||||
|
} from './gas-price-chart.utils.js' |
||||||
|
|
||||||
|
export default class GasPriceChart extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
gasPrices: PropTypes.array, |
||||||
|
estimatedTimes: PropTypes.array, |
||||||
|
gasPricesMax: PropTypes.number, |
||||||
|
estimatedTimesMax: PropTypes.number, |
||||||
|
currentPrice: PropTypes.number, |
||||||
|
updateCustomGasPrice: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
renderChart ({ |
||||||
|
currentPrice, |
||||||
|
gasPrices, |
||||||
|
estimatedTimes, |
||||||
|
gasPricesMax, |
||||||
|
estimatedTimesMax, |
||||||
|
updateCustomGasPrice, |
||||||
|
}) { |
||||||
|
const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) |
||||||
|
setTimeout(function () { |
||||||
|
setTickPosition('y', 0, -5, 8) |
||||||
|
setTickPosition('y', 1, -3, -5) |
||||||
|
setTickPosition('x', 0, 3) |
||||||
|
setTickPosition('x', 1, 3, -8) |
||||||
|
|
||||||
|
const { x: domainX } = getCoordinateData('.domain') |
||||||
|
const { x: yAxisX } = getCoordinateData('.c3-axis-y-label') |
||||||
|
const { x: tickX } = getCoordinateData('.c3-axis-x .tick') |
||||||
|
|
||||||
|
d3.select('.c3-axis-x .tick').attr('transform', 'translate(' + (domainX - tickX) / 2 + ', 0)') |
||||||
|
d3.select('.c3-axis-x-label').attr('transform', 'translate(0,-15)') |
||||||
|
d3.select('.c3-axis-y-label').attr('transform', 'translate(' + (domainX - yAxisX - 12) + ', 2) rotate(-90)') |
||||||
|
d3.select('.c3-xgrid-focus line').attr('y2', 98) |
||||||
|
|
||||||
|
d3.select('.c3-chart').on('mouseout', () => { |
||||||
|
hideDataUI(chart, '#overlayed-circle') |
||||||
|
}) |
||||||
|
|
||||||
|
d3.select('.c3-chart').on('click', () => { |
||||||
|
const { x: newGasPrice } = d3.select('#overlayed-circle').datum() |
||||||
|
updateCustomGasPrice(newGasPrice) |
||||||
|
}) |
||||||
|
|
||||||
|
const { x: chartXStart, width: chartWidth } = getCoordinateData('.c3-areas-data1') |
||||||
|
|
||||||
|
handleChartUpdate({ |
||||||
|
chart, |
||||||
|
gasPrices, |
||||||
|
newPrice: currentPrice, |
||||||
|
cssId: '#set-circle', |
||||||
|
}) |
||||||
|
|
||||||
|
d3.select('.c3-chart').on('mousemove', function () { |
||||||
|
handleMouseMove({ |
||||||
|
xMousePos: d3.event.clientX, |
||||||
|
chartXStart, |
||||||
|
chartWidth, |
||||||
|
gasPrices, |
||||||
|
estimatedTimes, |
||||||
|
chart, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, 0) |
||||||
|
|
||||||
|
this.chart = chart |
||||||
|
} |
||||||
|
|
||||||
|
componentDidUpdate (prevProps) { |
||||||
|
const { gasPrices, currentPrice: newPrice } = this.props |
||||||
|
|
||||||
|
if (prevProps.currentPrice !== newPrice) { |
||||||
|
handleChartUpdate({ |
||||||
|
chart: this.chart, |
||||||
|
gasPrices, |
||||||
|
newPrice, |
||||||
|
cssId: '#set-circle', |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount () { |
||||||
|
this.renderChart(this.props) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
return ( |
||||||
|
<div className="gas-price-chart" id="container"> |
||||||
|
<div className="gas-price-chart__root" id="chart"></div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,354 @@ |
|||||||
|
import * as d3 from 'd3' |
||||||
|
import c3 from 'c3' |
||||||
|
import BigNumber from 'bignumber.js' |
||||||
|
|
||||||
|
const newBigSigDig = n => (new BigNumber(n.toPrecision(15))) |
||||||
|
const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) |
||||||
|
const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus') |
||||||
|
const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div') |
||||||
|
|
||||||
|
export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) { |
||||||
|
const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({ |
||||||
|
xMousePos, |
||||||
|
chartXStart, |
||||||
|
chartWidth, |
||||||
|
gasPrices, |
||||||
|
estimatedTimes, |
||||||
|
}) |
||||||
|
|
||||||
|
if (currentPosValue === null && newTimeEstimate === null) { |
||||||
|
hideDataUI(chart, '#overlayed-circle') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const indexOfNewCircle = estimatedTimes.length + 1 |
||||||
|
const dataUIObj = generateDataUIObj(currentPosValue, indexOfNewCircle, newTimeEstimate) |
||||||
|
|
||||||
|
chart.internal.overlayPoint(dataUIObj, indexOfNewCircle) |
||||||
|
chart.internal.showTooltip([dataUIObj], d3.select('.c3-areas-data1')._groups[0]) |
||||||
|
chart.internal.showXGridFocus([dataUIObj]) |
||||||
|
} |
||||||
|
|
||||||
|
export function getCoordinateData (selector) { |
||||||
|
const node = d3.select(selector).node() |
||||||
|
return node ? node.getBoundingClientRect() : {} |
||||||
|
} |
||||||
|
|
||||||
|
export function generateDataUIObj (x, index, value) { |
||||||
|
return { |
||||||
|
x, |
||||||
|
value, |
||||||
|
index, |
||||||
|
id: 'data1', |
||||||
|
name: 'data1', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { |
||||||
|
const { |
||||||
|
closestLowerValueIndex, |
||||||
|
closestLowerValue, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue, |
||||||
|
} = getAdjacentGasPrices({ gasPrices, priceToPosition: newPrice }) |
||||||
|
|
||||||
|
if (closestLowerValue && closestHigherValue) { |
||||||
|
setSelectedCircle({ |
||||||
|
chart, |
||||||
|
newPrice, |
||||||
|
closestLowerValueIndex, |
||||||
|
closestLowerValue, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue, |
||||||
|
}) |
||||||
|
} else { |
||||||
|
hideDataUI(chart, cssId) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { |
||||||
|
const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) |
||||||
|
const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition) |
||||||
|
return { |
||||||
|
closestLowerValueIndex, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue: gasPrices[closestHigherValueIndex], |
||||||
|
closestLowerValue: gasPrices[closestLowerValueIndex], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { |
||||||
|
const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) |
||||||
|
const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() |
||||||
|
|
||||||
|
return newTimeEstimate.toNumber() |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { |
||||||
|
const chartMouseXPos = bigNumMinus(xMousePos, chartXStart) |
||||||
|
const posPercentile = bigNumDiv(chartMouseXPos, chartWidth) |
||||||
|
|
||||||
|
const currentPosValue = (bigNumMinus(gasPrices[gasPrices.length - 1], gasPrices[0])) |
||||||
|
.times(newBigSigDig(posPercentile)) |
||||||
|
.plus(newBigSigDig(gasPrices[0])) |
||||||
|
.toNumber() |
||||||
|
|
||||||
|
const { |
||||||
|
closestLowerValueIndex, |
||||||
|
closestLowerValue, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue, |
||||||
|
} = getAdjacentGasPrices({ gasPrices, priceToPosition: currentPosValue }) |
||||||
|
|
||||||
|
return !closestHigherValue || !closestLowerValue |
||||||
|
? { |
||||||
|
currentPosValue: null, |
||||||
|
newTimeEstimate: null, |
||||||
|
} |
||||||
|
: { |
||||||
|
currentPosValue, |
||||||
|
newTimeEstimate: extrapolateY({ |
||||||
|
higherY: estimatedTimes[closestHigherValueIndex], |
||||||
|
lowerY: estimatedTimes[closestLowerValueIndex], |
||||||
|
higherX: closestHigherValue, |
||||||
|
lowerX: closestLowerValue, |
||||||
|
xForExtrapolation: currentPosValue, |
||||||
|
}), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function hideDataUI (chart, dataNodeId) { |
||||||
|
const overLayedCircle = d3.select(dataNodeId) |
||||||
|
if (!overLayedCircle.empty()) { |
||||||
|
overLayedCircle.remove() |
||||||
|
} |
||||||
|
d3.select('.c3-tooltip-container').style('display', 'none !important') |
||||||
|
chart.internal.hideXGridFocus() |
||||||
|
} |
||||||
|
|
||||||
|
export function setTickPosition (axis, n, newPosition, secondNewPosition) { |
||||||
|
const positionToShift = axis === 'y' ? 'x' : 'y' |
||||||
|
const secondPositionToShift = axis === 'y' ? 'y' : 'x' |
||||||
|
d3.select('#chart') |
||||||
|
.select(`.c3-axis-${axis}`) |
||||||
|
.selectAll('.tick') |
||||||
|
.filter((d, i) => i === n) |
||||||
|
.select('text') |
||||||
|
.attr(positionToShift, 0) |
||||||
|
.select('tspan') |
||||||
|
.attr(positionToShift, newPosition) |
||||||
|
.attr(secondPositionToShift, secondNewPosition || 0) |
||||||
|
.style('visibility', 'visible') |
||||||
|
} |
||||||
|
|
||||||
|
export function appendOrUpdateCircle ({ data, itemIndex, cx, cy, cssId, appendOnly }) { |
||||||
|
const circle = this.main |
||||||
|
.select('.c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) |
||||||
|
.selectAll(`.c3-selected-circle-${itemIndex}`) |
||||||
|
|
||||||
|
if (appendOnly || circle.empty()) { |
||||||
|
circle.data([data]) |
||||||
|
.enter().append('circle') |
||||||
|
.attr('class', () => this.generateClass('c3-selected-circle', itemIndex)) |
||||||
|
.attr('id', cssId) |
||||||
|
.attr('cx', cx) |
||||||
|
.attr('cy', cy) |
||||||
|
.attr('stroke', () => this.color(data)) |
||||||
|
.attr('r', 6) |
||||||
|
} else { |
||||||
|
circle.data([data]) |
||||||
|
.attr('cx', cx) |
||||||
|
.attr('cy', cy) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setSelectedCircle ({ |
||||||
|
chart, |
||||||
|
newPrice, |
||||||
|
closestLowerValueIndex, |
||||||
|
closestLowerValue, |
||||||
|
closestHigherValueIndex, |
||||||
|
closestHigherValue, |
||||||
|
}) { |
||||||
|
const numberOfValues = chart.internal.data.xs.data1.length |
||||||
|
|
||||||
|
const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`) |
||||||
|
let { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`) |
||||||
|
let count = closestHigherValueIndex + 1 |
||||||
|
|
||||||
|
if (lowerX && higherX) { |
||||||
|
while (lowerX === higherX) { |
||||||
|
higherX = getCoordinateData(`.c3-circle-${count}`).x |
||||||
|
higherY = getCoordinateData(`.c3-circle-${count}`).y |
||||||
|
count++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const currentX = bigNumMinus(higherX, lowerX) |
||||||
|
.times(bigNumMinus(newPrice, closestLowerValue)) |
||||||
|
.div(bigNumMinus(closestHigherValue, closestLowerValue)) |
||||||
|
.plus(newBigSigDig(lowerX)) |
||||||
|
|
||||||
|
const newTimeEstimate = extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX }) |
||||||
|
|
||||||
|
chart.internal.selectPoint( |
||||||
|
generateDataUIObj(currentX.toNumber(), numberOfValues, newTimeEstimate), |
||||||
|
numberOfValues |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) { |
||||||
|
const gasPricesMaxPadded = gasPricesMax + 1 |
||||||
|
const chart = c3.generate({ |
||||||
|
size: { |
||||||
|
height: 165, |
||||||
|
}, |
||||||
|
transition: { |
||||||
|
duration: 0, |
||||||
|
}, |
||||||
|
padding: {left: 20, right: 15, top: 6, bottom: 10}, |
||||||
|
data: { |
||||||
|
x: 'x', |
||||||
|
columns: [ |
||||||
|
['x', ...gasPrices], |
||||||
|
['data1', ...estimatedTimes], |
||||||
|
], |
||||||
|
types: { |
||||||
|
data1: 'area', |
||||||
|
}, |
||||||
|
selection: { |
||||||
|
enabled: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
color: { |
||||||
|
data1: '#259de5', |
||||||
|
}, |
||||||
|
axis: { |
||||||
|
x: { |
||||||
|
min: gasPrices[0], |
||||||
|
max: gasPricesMax, |
||||||
|
tick: { |
||||||
|
values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], |
||||||
|
outer: false, |
||||||
|
format: function (val) { return val + ' GWEI' }, |
||||||
|
}, |
||||||
|
padding: {left: gasPricesMax / 50, right: gasPricesMax / 50}, |
||||||
|
label: { |
||||||
|
text: 'Gas Price ($)', |
||||||
|
position: 'outer-center', |
||||||
|
}, |
||||||
|
}, |
||||||
|
y: { |
||||||
|
padding: {top: 7, bottom: 7}, |
||||||
|
tick: { |
||||||
|
values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)], |
||||||
|
outer: false, |
||||||
|
}, |
||||||
|
label: { |
||||||
|
text: 'Confirmation time (sec)', |
||||||
|
position: 'outer-middle', |
||||||
|
}, |
||||||
|
min: 0, |
||||||
|
}, |
||||||
|
}, |
||||||
|
legend: { |
||||||
|
show: false, |
||||||
|
}, |
||||||
|
grid: { |
||||||
|
x: {}, |
||||||
|
lines: { |
||||||
|
front: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
point: { |
||||||
|
focus: { |
||||||
|
expand: { |
||||||
|
enabled: false, |
||||||
|
r: 3.5, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
format: { |
||||||
|
title: (v) => v.toPrecision(4), |
||||||
|
}, |
||||||
|
contents: function (d) { |
||||||
|
const titleFormat = this.config.tooltip_format_title |
||||||
|
let text |
||||||
|
d.forEach(el => { |
||||||
|
if (el && (el.value || el.value === 0) && !text) { |
||||||
|
text = "<table class='" + 'custom-tooltip' + "'>" + "<tr><th colspan='2'>" + titleFormat(el.x) + '</th></tr>' |
||||||
|
} |
||||||
|
}) |
||||||
|
return text + '</table>' + "<div class='tooltip-arrow'></div>" |
||||||
|
}, |
||||||
|
position: function (data) { |
||||||
|
if (d3.select('#overlayed-circle').empty()) { |
||||||
|
return { top: -100, left: -100 } |
||||||
|
} |
||||||
|
|
||||||
|
const { x: circleX, y: circleY, width: circleWidth } = getCoordinateData('#overlayed-circle') |
||||||
|
const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-chart') |
||||||
|
|
||||||
|
// TODO: Confirm the below constants work with all data sets and screen sizes
|
||||||
|
const flipTooltip = circleY - circleWidth < chartYStart + 5 |
||||||
|
|
||||||
|
d3 |
||||||
|
.select('.tooltip-arrow') |
||||||
|
.style('margin-top', flipTooltip ? '-16px' : '4px') |
||||||
|
|
||||||
|
return { |
||||||
|
top: bigNumMinus(circleY, chartYStart).minus(19).plus(flipTooltip ? circleWidth + 38 : 0).toNumber(), |
||||||
|
left: bigNumMinus(circleX, chartXStart).plus(newBigSigDig(circleWidth)).minus(bigNumDiv(gasPricesMaxPadded, 50)).toNumber(), |
||||||
|
} |
||||||
|
}, |
||||||
|
show: true, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) { |
||||||
|
const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-areas-data1') |
||||||
|
|
||||||
|
d3.select('#set-circle').remove() |
||||||
|
|
||||||
|
appendOrUpdateCircle.bind(this)({ |
||||||
|
data, |
||||||
|
itemIndex, |
||||||
|
cx: () => bigNumMinus(data.x, chartXStart).plus(11).toNumber(), |
||||||
|
cy: () => bigNumMinus(data.value, chartYStart).plus(10).toNumber(), |
||||||
|
cssId: 'set-circle', |
||||||
|
appendOnly: true, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
chart.internal.overlayPoint = function (data, itemIndex) { |
||||||
|
appendOrUpdateCircle.bind(this)({ |
||||||
|
data, |
||||||
|
itemIndex, |
||||||
|
cx: this.circleX.bind(this), |
||||||
|
cy: this.circleY.bind(this), |
||||||
|
cssId: 'overlayed-circle', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
chart.internal.showTooltip = function (selectedData, element) { |
||||||
|
const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0)) |
||||||
|
|
||||||
|
if (dataToShow.length) { |
||||||
|
this.tooltip.html( |
||||||
|
this.config.tooltip_contents.call(this, selectedData, this.axis.getXAxisTickFormat(), this.getYFormat(), this.color) |
||||||
|
).style('display', 'flex') |
||||||
|
|
||||||
|
// Get tooltip dimensions
|
||||||
|
const tWidth = this.tooltip.property('offsetWidth') |
||||||
|
const tHeight = this.tooltip.property('offsetHeight') |
||||||
|
const position = this.config.tooltip_position.call(this, dataToShow, tWidth, tHeight, element) |
||||||
|
// Set tooltip
|
||||||
|
this.tooltip.style('top', position.top + 'px').style('left', position.left + 'px') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return chart |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './gas-price-chart.component' |
@ -0,0 +1,132 @@ |
|||||||
|
.gas-price-chart { |
||||||
|
display: flex; |
||||||
|
position: relative; |
||||||
|
justify-content: center; |
||||||
|
|
||||||
|
&__root { |
||||||
|
max-height: 154px; |
||||||
|
max-width: 391px; |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
max-width: 326px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.tick text, .c3-axis-x-label, .c3-axis-y-label { |
||||||
|
font-family: Roboto; |
||||||
|
font-style: normal; |
||||||
|
font-weight: bold; |
||||||
|
line-height: normal; |
||||||
|
font-size: 8px; |
||||||
|
text-align: center; |
||||||
|
fill: #9A9CA6 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-tooltip-container { |
||||||
|
display: flex; |
||||||
|
justify-content: center !important; |
||||||
|
align-items: flex-end !important; |
||||||
|
} |
||||||
|
|
||||||
|
.custom-tooltip { |
||||||
|
background: rgba(0, 0, 0, 1); |
||||||
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); |
||||||
|
border-radius: 3px; |
||||||
|
opacity: 1 !important; |
||||||
|
height: 21px; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.tooltip-arrow { |
||||||
|
background: black; |
||||||
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); |
||||||
|
-webkit-transform: rotate(45deg); |
||||||
|
transform: rotate(45deg); |
||||||
|
opacity: 1 !important; |
||||||
|
width: 9px; |
||||||
|
height: 9px; |
||||||
|
margin-top: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.custom-tooltip th { |
||||||
|
font-family: Roboto; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 500; |
||||||
|
line-height: normal; |
||||||
|
font-size: 10px; |
||||||
|
text-align: center; |
||||||
|
padding: 3px; |
||||||
|
color: #FFFFFF; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-circle { |
||||||
|
visibility: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-selected-circle, .c3-circle._expanded_ { |
||||||
|
fill: #FFFFFF !important; |
||||||
|
stroke-width: 2.4px !important; |
||||||
|
stroke: #2d9fd9 !important; |
||||||
|
/* visibility: visible; */ |
||||||
|
} |
||||||
|
|
||||||
|
#set-circle { |
||||||
|
fill: #313A5E !important; |
||||||
|
stroke: #313A5E !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-axis-x-label, .c3-axis-y-label { |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.tick text tspan { |
||||||
|
visibility: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-circle { |
||||||
|
fill: #2d9fd9 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-line-data1 { |
||||||
|
stroke: #2d9fd9 !important; |
||||||
|
background: rgba(0,0,0,0) !important; |
||||||
|
color: rgba(0,0,0,0) !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3 path { |
||||||
|
fill: none; |
||||||
|
} |
||||||
|
|
||||||
|
.c3 path.c3-area-data1 { |
||||||
|
opacity: 1; |
||||||
|
fill: #e9edf1 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-xgrid-line line { |
||||||
|
stroke: #B8B8B8 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-xgrid-focus { |
||||||
|
stroke: #aaa; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-axis-x .domain { |
||||||
|
fill: none; |
||||||
|
stroke: none; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-axis-y .domain { |
||||||
|
fill: none; |
||||||
|
stroke: #C8CCD6; |
||||||
|
} |
||||||
|
|
||||||
|
.c3-event-rect { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#chart { |
||||||
|
background: #F8F9FB |
||||||
|
} |
@ -0,0 +1,218 @@ |
|||||||
|
import React from 'react' |
||||||
|
import assert from 'assert' |
||||||
|
import proxyquire from 'proxyquire' |
||||||
|
import sinon from 'sinon' |
||||||
|
import shallow from '../../../../../lib/shallow-with-context' |
||||||
|
import * as d3 from 'd3' |
||||||
|
|
||||||
|
function timeout (time) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
setTimeout(resolve, time) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const propsMethodSpies = { |
||||||
|
updateCustomGasPrice: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const selectReturnSpies = { |
||||||
|
empty: sinon.spy(), |
||||||
|
remove: sinon.spy(), |
||||||
|
style: sinon.spy(), |
||||||
|
select: d3.select, |
||||||
|
attr: sinon.spy(), |
||||||
|
on: sinon.spy(), |
||||||
|
datum: sinon.stub().returns({ x: 'mockX' }), |
||||||
|
} |
||||||
|
|
||||||
|
const mockSelectReturn = { |
||||||
|
...d3.select('div'), |
||||||
|
node: () => ({ |
||||||
|
getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }), |
||||||
|
}), |
||||||
|
...selectReturnSpies, |
||||||
|
} |
||||||
|
|
||||||
|
const gasPriceChartUtilsSpies = { |
||||||
|
appendOrUpdateCircle: sinon.spy(), |
||||||
|
generateChart: sinon.stub().returns({ mockChart: true }), |
||||||
|
generateDataUIObj: sinon.spy(), |
||||||
|
getAdjacentGasPrices: sinon.spy(), |
||||||
|
getCoordinateData: sinon.stub().returns({ x: 'mockCoordinateX', width: 'mockWidth' }), |
||||||
|
getNewXandTimeEstimate: sinon.spy(), |
||||||
|
handleChartUpdate: sinon.spy(), |
||||||
|
hideDataUI: sinon.spy(), |
||||||
|
setSelectedCircle: sinon.spy(), |
||||||
|
setTickPosition: sinon.spy(), |
||||||
|
handleMouseMove: sinon.spy(), |
||||||
|
} |
||||||
|
|
||||||
|
const testProps = { |
||||||
|
gasPrices: [1.5, 2.5, 4, 8], |
||||||
|
estimatedTimes: [100, 80, 40, 10], |
||||||
|
gasPricesMax: 9, |
||||||
|
estimatedTimesMax: '100', |
||||||
|
currentPrice: 6, |
||||||
|
updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, |
||||||
|
} |
||||||
|
|
||||||
|
const GasPriceChart = proxyquire('../gas-price-chart.component.js', { |
||||||
|
'./gas-price-chart.utils.js': gasPriceChartUtilsSpies, |
||||||
|
'd3': { |
||||||
|
...d3, |
||||||
|
select: function (...args) { |
||||||
|
const result = d3.select(...args) |
||||||
|
return result.empty() |
||||||
|
? mockSelectReturn |
||||||
|
: result |
||||||
|
}, |
||||||
|
event: { |
||||||
|
clientX: 'mockClientX', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}).default |
||||||
|
|
||||||
|
sinon.spy(GasPriceChart.prototype, 'renderChart') |
||||||
|
|
||||||
|
describe('GasPriceChart Component', function () { |
||||||
|
let wrapper |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
wrapper = shallow(<GasPriceChart {...testProps} />) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('render()', () => { |
||||||
|
it('should render', () => { |
||||||
|
assert(wrapper.hasClass('gas-price-chart')) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should render the chart div', () => { |
||||||
|
assert(wrapper.childAt(0).hasClass('gas-price-chart__root')) |
||||||
|
assert.equal(wrapper.childAt(0).props().id, 'chart') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('componentDidMount', () => { |
||||||
|
it('should call this.renderChart with the components props', () => { |
||||||
|
assert(GasPriceChart.prototype.renderChart.callCount, 1) |
||||||
|
wrapper.instance().componentDidMount() |
||||||
|
assert(GasPriceChart.prototype.renderChart.callCount, 2) |
||||||
|
assert.deepEqual(GasPriceChart.prototype.renderChart.getCall(1).args, [{...testProps}]) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('componentDidUpdate', () => { |
||||||
|
it('should call handleChartUpdate if props.currentPrice has changed', () => { |
||||||
|
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() |
||||||
|
wrapper.instance().componentDidUpdate({ currentPrice: 7 }) |
||||||
|
assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call handleChartUpdate with the correct props', () => { |
||||||
|
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() |
||||||
|
wrapper.instance().componentDidUpdate({ currentPrice: 7 }) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{ |
||||||
|
chart: { mockChart: true }, |
||||||
|
gasPrices: [1.5, 2.5, 4, 8], |
||||||
|
newPrice: 6, |
||||||
|
cssId: '#set-circle', |
||||||
|
}]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should not call handleChartUpdate if props.currentPrice has not changed', () => { |
||||||
|
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() |
||||||
|
wrapper.instance().componentDidUpdate({ currentPrice: 6 }) |
||||||
|
assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 0) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('renderChart', () => { |
||||||
|
it('should call setTickPosition 4 times, with the expected props', async () => { |
||||||
|
await timeout(0) |
||||||
|
gasPriceChartUtilsSpies.setTickPosition.resetHistory() |
||||||
|
assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 0) |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
await timeout(0) |
||||||
|
assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 4) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(0).args, ['y', 0, -5, 8]) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(1).args, ['y', 1, -3, -5]) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3]) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call handleChartUpdate with the correct props', async () => { |
||||||
|
await timeout(0) |
||||||
|
gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
await timeout(0) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{ |
||||||
|
chart: { mockChart: true }, |
||||||
|
gasPrices: [1.5, 2.5, 4, 8], |
||||||
|
newPrice: 6, |
||||||
|
cssId: '#set-circle', |
||||||
|
}]) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should add three events to the chart', async () => { |
||||||
|
await timeout(0) |
||||||
|
selectReturnSpies.on.resetHistory() |
||||||
|
assert.equal(selectReturnSpies.on.callCount, 0) |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
await timeout(0) |
||||||
|
assert.equal(selectReturnSpies.on.callCount, 3) |
||||||
|
|
||||||
|
const firstOnEventArgs = selectReturnSpies.on.getCall(0).args |
||||||
|
assert.equal(firstOnEventArgs[0], 'mouseout') |
||||||
|
const secondOnEventArgs = selectReturnSpies.on.getCall(1).args |
||||||
|
assert.equal(secondOnEventArgs[0], 'click') |
||||||
|
const thirdOnEventArgs = selectReturnSpies.on.getCall(2).args |
||||||
|
assert.equal(thirdOnEventArgs[0], 'mousemove') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should hide the data UI on mouseout', async () => { |
||||||
|
await timeout(0) |
||||||
|
selectReturnSpies.on.resetHistory() |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
gasPriceChartUtilsSpies.hideDataUI.resetHistory() |
||||||
|
await timeout(0) |
||||||
|
const mouseoutEventArgs = selectReturnSpies.on.getCall(0).args |
||||||
|
assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 0) |
||||||
|
mouseoutEventArgs[1]() |
||||||
|
assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 1) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.hideDataUI.getCall(0).args, [{ mockChart: true }, '#overlayed-circle']) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should updateCustomGasPrice on click', async () => { |
||||||
|
await timeout(0) |
||||||
|
selectReturnSpies.on.resetHistory() |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
propsMethodSpies.updateCustomGasPrice.resetHistory() |
||||||
|
await timeout(0) |
||||||
|
const mouseoutEventArgs = selectReturnSpies.on.getCall(1).args |
||||||
|
assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 0) |
||||||
|
mouseoutEventArgs[1]() |
||||||
|
assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 1) |
||||||
|
assert.equal(propsMethodSpies.updateCustomGasPrice.getCall(0).args[0], 'mockX') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should handle mousemove', async () => { |
||||||
|
await timeout(0) |
||||||
|
selectReturnSpies.on.resetHistory() |
||||||
|
wrapper.instance().renderChart(testProps) |
||||||
|
gasPriceChartUtilsSpies.handleMouseMove.resetHistory() |
||||||
|
await timeout(0) |
||||||
|
const mouseoutEventArgs = selectReturnSpies.on.getCall(2).args |
||||||
|
assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 0) |
||||||
|
mouseoutEventArgs[1]() |
||||||
|
assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 1) |
||||||
|
assert.deepEqual(gasPriceChartUtilsSpies.handleMouseMove.getCall(0).args, [{ |
||||||
|
xMousePos: 'mockClientX', |
||||||
|
chartXStart: 'mockCoordinateX', |
||||||
|
chartWidth: 'mockWidth', |
||||||
|
gasPrices: testProps.gasPrices, |
||||||
|
estimatedTimes: testProps.estimatedTimes, |
||||||
|
chart: { mockChart: true }, |
||||||
|
}]) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,48 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
|
||||||
|
export default class AdvancedTabContent extends Component { |
||||||
|
static propTypes = { |
||||||
|
onChange: PropTypes.func, |
||||||
|
lowLabel: PropTypes.string, |
||||||
|
highLabel: PropTypes.string, |
||||||
|
value: PropTypes.number, |
||||||
|
step: PropTypes.number, |
||||||
|
max: PropTypes.number, |
||||||
|
min: PropTypes.number, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
onChange, |
||||||
|
lowLabel, |
||||||
|
highLabel, |
||||||
|
value, |
||||||
|
step, |
||||||
|
max, |
||||||
|
min, |
||||||
|
} = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="gas-slider"> |
||||||
|
<input |
||||||
|
className="gas-slider__input" |
||||||
|
type="range" |
||||||
|
step={step} |
||||||
|
max={max} |
||||||
|
min={min} |
||||||
|
value={value} |
||||||
|
id="gasSlider" |
||||||
|
onChange={event => onChange(event.target.value)} |
||||||
|
/> |
||||||
|
<div className="gas-slider__bar"> |
||||||
|
<div className="gas-slider__colored"/> |
||||||
|
</div> |
||||||
|
<div className="gas-slider__labels"> |
||||||
|
<span>{lowLabel}</span> |
||||||
|
<span>{highLabel}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './gas-slider.component' |
@ -0,0 +1,54 @@ |
|||||||
|
.gas-slider { |
||||||
|
position: relative; |
||||||
|
width: 322px; |
||||||
|
|
||||||
|
&__input { |
||||||
|
width: 322px; |
||||||
|
margin-left: -2px; |
||||||
|
z-index: 2; |
||||||
|
} |
||||||
|
|
||||||
|
input[type=range] { |
||||||
|
-webkit-appearance: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb { |
||||||
|
-webkit-appearance: none !important; |
||||||
|
height: 34px; |
||||||
|
width: 34px; |
||||||
|
background-color: $curious-blue; |
||||||
|
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); |
||||||
|
border-radius: 50%; |
||||||
|
position: relative; |
||||||
|
z-index: 10; |
||||||
|
} |
||||||
|
|
||||||
|
&__bar { |
||||||
|
height: 6px; |
||||||
|
width: 322px; |
||||||
|
background: $alto; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
position: absolute; |
||||||
|
top: 16px; |
||||||
|
z-index: 0; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
&__colored { |
||||||
|
height: 6px; |
||||||
|
border-radius: 4px; |
||||||
|
margin-left: 102px; |
||||||
|
width: 322px; |
||||||
|
z-index: 1; |
||||||
|
background-color: $blizzard-blue; |
||||||
|
} |
||||||
|
|
||||||
|
&__labels { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
font-size: 12px; |
||||||
|
margin-top: -6px; |
||||||
|
color: $mid-gray; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
const selectors = { |
||||||
|
getCurrentBlockTime, |
||||||
|
getBasicGasEstimateLoadingStatus, |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = selectors |
||||||
|
|
||||||
|
function getCurrentBlockTime (state) { |
||||||
|
return state.gas.currentBlockTime |
||||||
|
} |
||||||
|
|
||||||
|
function getBasicGasEstimateLoadingStatus (state) { |
||||||
|
return state.gas.basicEstimateIsLoading |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
@import './gas-slider/index'; |
||||||
|
|
||||||
|
@import './gas-modal-page-container/index'; |
||||||
|
|
||||||
|
@import './gas-price-chart/index'; |
@ -0,0 +1,112 @@ |
|||||||
|
.sidebar-left { |
||||||
|
display: flex; |
||||||
|
|
||||||
|
.gas-modal-page-container { |
||||||
|
display: flex; |
||||||
|
|
||||||
|
.page-container { |
||||||
|
flex: 1; |
||||||
|
max-width: 100%; |
||||||
|
|
||||||
|
&__content { |
||||||
|
display: flex; |
||||||
|
overflow-y: initial; |
||||||
|
} |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
max-width: 344px; |
||||||
|
min-height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
@media screen and (min-width: $break-small) { |
||||||
|
max-height: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.gas-price-chart { |
||||||
|
margin-left: 10px; |
||||||
|
|
||||||
|
&__root { |
||||||
|
max-height: 160px !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.page-container__bottom { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
flex-flow: space-between; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.page-container__content { |
||||||
|
overflow-y: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
.basic-tab-content { |
||||||
|
height: auto; |
||||||
|
margin-bottom: 0px; |
||||||
|
border-bottom: 1px solid #d2d8dd; |
||||||
|
flex: 1 1 70%; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
padding-left: 14px; |
||||||
|
padding-bottom: 21px; |
||||||
|
} |
||||||
|
|
||||||
|
.gas-price-button-group--alt { |
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
max-width: 318px; |
||||||
|
|
||||||
|
&__time-estimate { |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.advanced-tab { |
||||||
|
@media screen and (min-width: $break-small) { |
||||||
|
flex: 1 1 70%; |
||||||
|
} |
||||||
|
|
||||||
|
&__fee-chart { |
||||||
|
height: 320px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
height: initial; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__fee-chart__speed-buttons { |
||||||
|
bottom: 77px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.gas-modal-content { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
&__info-row-wrapper { |
||||||
|
display: flex; |
||||||
|
@media screen and (min-width: $break-small) { |
||||||
|
flex: 1 1 30%; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__info-row { |
||||||
|
height: 170px; |
||||||
|
|
||||||
|
@media screen and (max-width: $break-small) { |
||||||
|
height: initial; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,51 +0,0 @@ |
|||||||
.gas-slider { |
|
||||||
position: relative; |
|
||||||
width: 313px; |
|
||||||
|
|
||||||
&__input { |
|
||||||
width: 317px; |
|
||||||
margin-left: -2px; |
|
||||||
z-index: 2; |
|
||||||
} |
|
||||||
|
|
||||||
input[type=range] { |
|
||||||
-webkit-appearance: none !important; |
|
||||||
} |
|
||||||
|
|
||||||
input[type=range]::-webkit-slider-thumb { |
|
||||||
-webkit-appearance: none !important; |
|
||||||
height: 26px; |
|
||||||
width: 26px; |
|
||||||
border: 2px solid #B8B8B8; |
|
||||||
background-color: #FFFFFF; |
|
||||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); |
|
||||||
border-radius: 50%; |
|
||||||
position: relative; |
|
||||||
z-index: 10; |
|
||||||
} |
|
||||||
|
|
||||||
&__bar { |
|
||||||
height: 6px; |
|
||||||
width: 313px; |
|
||||||
background: $alto; |
|
||||||
display: flex; |
|
||||||
justify-content: space-between; |
|
||||||
position: absolute; |
|
||||||
top: 11px; |
|
||||||
z-index: 0; |
|
||||||
} |
|
||||||
|
|
||||||
&__low, &__high { |
|
||||||
height: 6px; |
|
||||||
width: 49px; |
|
||||||
z-index: 1; |
|
||||||
} |
|
||||||
|
|
||||||
&__low { |
|
||||||
background-color: $crimson; |
|
||||||
} |
|
||||||
|
|
||||||
&__high { |
|
||||||
background-color: $caribbean-green; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,468 @@ |
|||||||
|
import { clone, uniqBy, flatten } from 'ramda' |
||||||
|
import BigNumber from 'bignumber.js' |
||||||
|
import { |
||||||
|
loadLocalStorageData, |
||||||
|
saveLocalStorageData, |
||||||
|
} from '../../lib/local-storage-helpers' |
||||||
|
|
||||||
|
// Actions
|
||||||
|
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' |
||||||
|
const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED' |
||||||
|
const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED' |
||||||
|
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED' |
||||||
|
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE' |
||||||
|
const RESET_CUSTOM_DATA = 'metamask/gas/RESET_CUSTOM_DATA' |
||||||
|
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA' |
||||||
|
const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS' |
||||||
|
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT' |
||||||
|
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE' |
||||||
|
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL' |
||||||
|
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES' |
||||||
|
const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' |
||||||
|
const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED' |
||||||
|
|
||||||
|
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
|
||||||
|
const initState = { |
||||||
|
customData: { |
||||||
|
price: null, |
||||||
|
limit: '0x5208', |
||||||
|
}, |
||||||
|
basicEstimates: { |
||||||
|
average: null, |
||||||
|
fastestWait: null, |
||||||
|
fastWait: null, |
||||||
|
fast: null, |
||||||
|
safeLowWait: null, |
||||||
|
blockNum: null, |
||||||
|
avgWait: null, |
||||||
|
blockTime: null, |
||||||
|
speed: null, |
||||||
|
fastest: null, |
||||||
|
safeLow: null, |
||||||
|
}, |
||||||
|
basicEstimateIsLoading: true, |
||||||
|
gasEstimatesLoading: true, |
||||||
|
priceAndTimeEstimates: [], |
||||||
|
basicPriceAndTimeEstimates: [], |
||||||
|
priceAndTimeEstimatesLastRetrieved: 0, |
||||||
|
basicPriceAndTimeEstimatesLastRetrieved: 0, |
||||||
|
errors: {}, |
||||||
|
} |
||||||
|
|
||||||
|
// Reducer
|
||||||
|
export default function reducer ({ gas: gasState = initState }, action = {}) { |
||||||
|
const newState = clone(gasState) |
||||||
|
|
||||||
|
switch (action.type) { |
||||||
|
case BASIC_GAS_ESTIMATE_LOADING_STARTED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
basicEstimateIsLoading: true, |
||||||
|
} |
||||||
|
case BASIC_GAS_ESTIMATE_LOADING_FINISHED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
basicEstimateIsLoading: false, |
||||||
|
} |
||||||
|
case GAS_ESTIMATE_LOADING_STARTED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
gasEstimatesLoading: true, |
||||||
|
} |
||||||
|
case GAS_ESTIMATE_LOADING_FINISHED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
gasEstimatesLoading: false, |
||||||
|
} |
||||||
|
case SET_BASIC_GAS_ESTIMATE_DATA: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
basicEstimates: action.value, |
||||||
|
} |
||||||
|
case SET_CUSTOM_GAS_PRICE: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
customData: { |
||||||
|
...newState.customData, |
||||||
|
price: action.value, |
||||||
|
}, |
||||||
|
} |
||||||
|
case SET_CUSTOM_GAS_LIMIT: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
customData: { |
||||||
|
...newState.customData, |
||||||
|
limit: action.value, |
||||||
|
}, |
||||||
|
} |
||||||
|
case SET_CUSTOM_GAS_TOTAL: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
customData: { |
||||||
|
...newState.customData, |
||||||
|
total: action.value, |
||||||
|
}, |
||||||
|
} |
||||||
|
case SET_PRICE_AND_TIME_ESTIMATES: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
priceAndTimeEstimates: action.value, |
||||||
|
} |
||||||
|
case SET_CUSTOM_GAS_ERRORS: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
errors: { |
||||||
|
...newState.errors, |
||||||
|
...action.value, |
||||||
|
}, |
||||||
|
} |
||||||
|
case SET_API_ESTIMATES_LAST_RETRIEVED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
priceAndTimeEstimatesLastRetrieved: action.value, |
||||||
|
} |
||||||
|
case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
basicPriceAndTimeEstimatesLastRetrieved: action.value, |
||||||
|
} |
||||||
|
case RESET_CUSTOM_DATA: |
||||||
|
return { |
||||||
|
...newState, |
||||||
|
customData: clone(initState.customData), |
||||||
|
} |
||||||
|
case RESET_CUSTOM_GAS_STATE: |
||||||
|
return clone(initState) |
||||||
|
default: |
||||||
|
return newState |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
export function basicGasEstimatesLoadingStarted () { |
||||||
|
return { |
||||||
|
type: BASIC_GAS_ESTIMATE_LOADING_STARTED, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function basicGasEstimatesLoadingFinished () { |
||||||
|
return { |
||||||
|
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function gasEstimatesLoadingStarted () { |
||||||
|
return { |
||||||
|
type: GAS_ESTIMATE_LOADING_STARTED, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function gasEstimatesLoadingFinished () { |
||||||
|
return { |
||||||
|
type: GAS_ESTIMATE_LOADING_FINISHED, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function fetchBasicGasEstimates () { |
||||||
|
return (dispatch) => { |
||||||
|
dispatch(basicGasEstimatesLoadingStarted()) |
||||||
|
|
||||||
|
return fetch('https://dev.blockscale.net/api/gasexpress.json', { |
||||||
|
'headers': {}, |
||||||
|
'referrer': 'https://dev.blockscale.net/api/', |
||||||
|
'referrerPolicy': 'no-referrer-when-downgrade', |
||||||
|
'body': null, |
||||||
|
'method': 'GET', |
||||||
|
'mode': 'cors'} |
||||||
|
) |
||||||
|
.then(r => r.json()) |
||||||
|
.then(({ |
||||||
|
safeLow, |
||||||
|
standard: average, |
||||||
|
fast, |
||||||
|
fastest, |
||||||
|
block_time: blockTime, |
||||||
|
blockNum, |
||||||
|
}) => { |
||||||
|
const basicEstimates = { |
||||||
|
safeLow, |
||||||
|
average, |
||||||
|
fast, |
||||||
|
fastest, |
||||||
|
blockTime, |
||||||
|
blockNum, |
||||||
|
} |
||||||
|
dispatch(setBasicGasEstimateData(basicEstimates)) |
||||||
|
dispatch(basicGasEstimatesLoadingFinished()) |
||||||
|
return basicEstimates |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function fetchBasicGasAndTimeEstimates () { |
||||||
|
return (dispatch, getState) => { |
||||||
|
const { |
||||||
|
basicPriceAndTimeEstimatesLastRetrieved, |
||||||
|
basicPriceAndTimeEstimates, |
||||||
|
} = getState().gas |
||||||
|
const timeLastRetrieved = basicPriceAndTimeEstimatesLastRetrieved || loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') || 0 |
||||||
|
|
||||||
|
dispatch(basicGasEstimatesLoadingStarted()) |
||||||
|
|
||||||
|
const promiseToFetch = Date.now() - timeLastRetrieved > 75000 |
||||||
|
? fetch('https://ethgasstation.info/json/ethgasAPI.json', { |
||||||
|
'headers': {}, |
||||||
|
'referrer': 'http://ethgasstation.info/json/', |
||||||
|
'referrerPolicy': 'no-referrer-when-downgrade', |
||||||
|
'body': null, |
||||||
|
'method': 'GET', |
||||||
|
'mode': 'cors'} |
||||||
|
) |
||||||
|
.then(r => r.json()) |
||||||
|
.then(({ |
||||||
|
average: averageTimes10, |
||||||
|
avgWait, |
||||||
|
block_time: blockTime, |
||||||
|
blockNum, |
||||||
|
fast: fastTimes10, |
||||||
|
fastest: fastestTimes10, |
||||||
|
fastestWait, |
||||||
|
fastWait, |
||||||
|
safeLow: safeLowTimes10, |
||||||
|
safeLowWait, |
||||||
|
speed, |
||||||
|
}) => { |
||||||
|
const [average, fast, fastest, safeLow] = [ |
||||||
|
averageTimes10, |
||||||
|
fastTimes10, |
||||||
|
fastestTimes10, |
||||||
|
safeLowTimes10, |
||||||
|
].map(price => (new BigNumber(price)).div(10).toNumber()) |
||||||
|
|
||||||
|
const basicEstimates = { |
||||||
|
average, |
||||||
|
avgWait, |
||||||
|
blockTime, |
||||||
|
blockNum, |
||||||
|
fast, |
||||||
|
fastest, |
||||||
|
fastestWait, |
||||||
|
fastWait, |
||||||
|
safeLow, |
||||||
|
safeLowWait, |
||||||
|
speed, |
||||||
|
} |
||||||
|
|
||||||
|
const timeRetrieved = Date.now() |
||||||
|
dispatch(setBasicApiEstimatesLastRetrieved(timeRetrieved)) |
||||||
|
saveLocalStorageData(timeRetrieved, 'BASIC_GAS_AND_TIME_API_ESTIMATES_LAST_RETRIEVED') |
||||||
|
saveLocalStorageData(basicEstimates, 'BASIC_GAS_AND_TIME_API_ESTIMATES') |
||||||
|
|
||||||
|
return basicEstimates |
||||||
|
}) |
||||||
|
: Promise.resolve(basicPriceAndTimeEstimates.length |
||||||
|
? basicPriceAndTimeEstimates |
||||||
|
: loadLocalStorageData('BASIC_GAS_AND_TIME_API_ESTIMATES') |
||||||
|
) |
||||||
|
|
||||||
|
return promiseToFetch.then(basicEstimates => { |
||||||
|
dispatch(setBasicGasEstimateData(basicEstimates)) |
||||||
|
dispatch(basicGasEstimatesLoadingFinished()) |
||||||
|
return basicEstimates |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) { |
||||||
|
higherY = new BigNumber(higherY, 10) |
||||||
|
lowerY = new BigNumber(lowerY, 10) |
||||||
|
higherX = new BigNumber(higherX, 10) |
||||||
|
lowerX = new BigNumber(lowerX, 10) |
||||||
|
xForExtrapolation = new BigNumber(xForExtrapolation, 10) |
||||||
|
const slope = (higherY.minus(lowerY)).div(higherX.minus(lowerX)) |
||||||
|
const newTimeEstimate = slope.times(higherX.minus(xForExtrapolation)).minus(higherY).negated() |
||||||
|
|
||||||
|
return Number(newTimeEstimate.toPrecision(10)) |
||||||
|
} |
||||||
|
|
||||||
|
function getRandomArbitrary (min, max) { |
||||||
|
min = new BigNumber(min, 10) |
||||||
|
max = new BigNumber(max, 10) |
||||||
|
const random = new BigNumber(String(Math.random()), 10) |
||||||
|
return new BigNumber(random.times(max.minus(min)).plus(min)).toPrecision(10) |
||||||
|
} |
||||||
|
|
||||||
|
function calcMedian (list) { |
||||||
|
const medianPos = (Math.floor(list.length / 2) + Math.ceil(list.length / 2)) / 2 |
||||||
|
return medianPos === Math.floor(medianPos) |
||||||
|
? (list[medianPos - 1] + list[medianPos]) / 2 |
||||||
|
: list[Math.floor(medianPos)] |
||||||
|
} |
||||||
|
|
||||||
|
function quartiles (data) { |
||||||
|
const lowerHalf = data.slice(0, Math.floor(data.length / 2)) |
||||||
|
const upperHalf = data.slice(Math.floor(data.length / 2) + (data.length % 2 === 0 ? 0 : 1)) |
||||||
|
const median = calcMedian(data) |
||||||
|
const lowerQuartile = calcMedian(lowerHalf) |
||||||
|
const upperQuartile = calcMedian(upperHalf) |
||||||
|
return { |
||||||
|
median, |
||||||
|
lowerQuartile, |
||||||
|
upperQuartile, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function inliersByIQR (data, prop) { |
||||||
|
const { lowerQuartile, upperQuartile } = quartiles(data.map(d => prop ? d[prop] : d)) |
||||||
|
const IQR = upperQuartile - lowerQuartile |
||||||
|
const lowerBound = lowerQuartile - 1.5 * IQR |
||||||
|
const upperBound = upperQuartile + 1.5 * IQR |
||||||
|
return data.filter(d => { |
||||||
|
const value = prop ? d[prop] : d |
||||||
|
return value >= lowerBound && value <= upperBound |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function fetchGasEstimates (blockTime) { |
||||||
|
return (dispatch, getState) => { |
||||||
|
const { |
||||||
|
priceAndTimeEstimatesLastRetrieved, |
||||||
|
priceAndTimeEstimates, |
||||||
|
} = getState().gas |
||||||
|
const timeLastRetrieved = priceAndTimeEstimatesLastRetrieved || loadLocalStorageData('GAS_API_ESTIMATES_LAST_RETRIEVED') || 0 |
||||||
|
|
||||||
|
dispatch(gasEstimatesLoadingStarted()) |
||||||
|
|
||||||
|
const promiseToFetch = Date.now() - timeLastRetrieved > 75000 |
||||||
|
? fetch('https://ethgasstation.info/json/predictTable.json', { |
||||||
|
'headers': {}, |
||||||
|
'referrer': 'http://ethgasstation.info/json/', |
||||||
|
'referrerPolicy': 'no-referrer-when-downgrade', |
||||||
|
'body': null, |
||||||
|
'method': 'GET', |
||||||
|
'mode': 'cors'} |
||||||
|
) |
||||||
|
.then(r => r.json()) |
||||||
|
.then(r => { |
||||||
|
const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice })) |
||||||
|
const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes) |
||||||
|
|
||||||
|
const withSupplementalTimeEstimates = flatten(estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }, i, arr) => { |
||||||
|
const next = arr[i + 1] |
||||||
|
if (!next) { |
||||||
|
return [{ expectedWait, gasprice }] |
||||||
|
} else { |
||||||
|
const supplementalPrice = getRandomArbitrary(gasprice, next.gasprice) |
||||||
|
const supplementalTime = extrapolateY({ |
||||||
|
higherY: next.expectedWait, |
||||||
|
lowerY: expectedWait, |
||||||
|
higherX: next.gasprice, |
||||||
|
lowerX: gasprice, |
||||||
|
xForExtrapolation: supplementalPrice, |
||||||
|
}) |
||||||
|
const supplementalPrice2 = getRandomArbitrary(supplementalPrice, next.gasprice) |
||||||
|
const supplementalTime2 = extrapolateY({ |
||||||
|
higherY: next.expectedWait, |
||||||
|
lowerY: supplementalTime, |
||||||
|
higherX: next.gasprice, |
||||||
|
lowerX: supplementalPrice, |
||||||
|
xForExtrapolation: supplementalPrice2, |
||||||
|
}) |
||||||
|
return [ |
||||||
|
{ expectedWait, gasprice }, |
||||||
|
{ expectedWait: supplementalTime, gasprice: supplementalPrice }, |
||||||
|
{ expectedWait: supplementalTime2, gasprice: supplementalPrice2 }, |
||||||
|
] |
||||||
|
} |
||||||
|
})) |
||||||
|
const withOutliersRemoved = inliersByIQR(withSupplementalTimeEstimates.slice(0).reverse(), 'expectedWait').reverse() |
||||||
|
const timeMappedToSeconds = withOutliersRemoved.map(({ expectedWait, gasprice }) => { |
||||||
|
const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toNumber() |
||||||
|
return { |
||||||
|
expectedTime, |
||||||
|
gasprice: (new BigNumber(gasprice, 10).toNumber()), |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const timeRetrieved = Date.now() |
||||||
|
dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) |
||||||
|
saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED') |
||||||
|
saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES') |
||||||
|
|
||||||
|
return timeMappedToSeconds |
||||||
|
}) |
||||||
|
: Promise.resolve(priceAndTimeEstimates.length |
||||||
|
? priceAndTimeEstimates |
||||||
|
: loadLocalStorageData('GAS_API_ESTIMATES') |
||||||
|
) |
||||||
|
|
||||||
|
return promiseToFetch.then(estimates => { |
||||||
|
dispatch(setPricesAndTimeEstimates(estimates)) |
||||||
|
dispatch(gasEstimatesLoadingFinished()) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setBasicGasEstimateData (basicGasEstimateData) { |
||||||
|
return { |
||||||
|
type: SET_BASIC_GAS_ESTIMATE_DATA, |
||||||
|
value: basicGasEstimateData, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setPricesAndTimeEstimates (estimatedPricesAndTimes) { |
||||||
|
return { |
||||||
|
type: SET_PRICE_AND_TIME_ESTIMATES, |
||||||
|
value: estimatedPricesAndTimes, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setCustomGasPrice (newPrice) { |
||||||
|
return { |
||||||
|
type: SET_CUSTOM_GAS_PRICE, |
||||||
|
value: newPrice, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setCustomGasLimit (newLimit) { |
||||||
|
return { |
||||||
|
type: SET_CUSTOM_GAS_LIMIT, |
||||||
|
value: newLimit, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setCustomGasTotal (newTotal) { |
||||||
|
return { |
||||||
|
type: SET_CUSTOM_GAS_TOTAL, |
||||||
|
value: newTotal, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setCustomGasErrors (newErrors) { |
||||||
|
return { |
||||||
|
type: SET_CUSTOM_GAS_ERRORS, |
||||||
|
value: newErrors, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setApiEstimatesLastRetrieved (retrievalTime) { |
||||||
|
return { |
||||||
|
type: SET_API_ESTIMATES_LAST_RETRIEVED, |
||||||
|
value: retrievalTime, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setBasicApiEstimatesLastRetrieved (retrievalTime) { |
||||||
|
return { |
||||||
|
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, |
||||||
|
value: retrievalTime, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function resetCustomGasState () { |
||||||
|
return { type: RESET_CUSTOM_GAS_STATE } |
||||||
|
} |
||||||
|
|
||||||
|
export function resetCustomData () { |
||||||
|
return { type: RESET_CUSTOM_DATA } |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,544 @@ |
|||||||
|
import assert from 'assert' |
||||||
|
import sinon from 'sinon' |
||||||
|
import proxyquire from 'proxyquire' |
||||||
|
|
||||||
|
|
||||||
|
const GasDuck = proxyquire('../gas.duck.js', { |
||||||
|
'../../lib/local-storage-helpers': { |
||||||
|
loadLocalStorageData: sinon.spy(), |
||||||
|
saveLocalStorageData: sinon.spy(), |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const { |
||||||
|
basicGasEstimatesLoadingStarted, |
||||||
|
basicGasEstimatesLoadingFinished, |
||||||
|
setBasicGasEstimateData, |
||||||
|
setCustomGasPrice, |
||||||
|
setCustomGasLimit, |
||||||
|
setCustomGasTotal, |
||||||
|
setCustomGasErrors, |
||||||
|
resetCustomGasState, |
||||||
|
fetchBasicGasAndTimeEstimates, |
||||||
|
gasEstimatesLoadingStarted, |
||||||
|
gasEstimatesLoadingFinished, |
||||||
|
setPricesAndTimeEstimates, |
||||||
|
fetchGasEstimates, |
||||||
|
setApiEstimatesLastRetrieved, |
||||||
|
} = GasDuck |
||||||
|
const GasReducer = GasDuck.default |
||||||
|
|
||||||
|
describe('Gas Duck', () => { |
||||||
|
let tempFetch |
||||||
|
let tempDateNow |
||||||
|
const mockEthGasApiResponse = { |
||||||
|
average: 20, |
||||||
|
avgWait: 'mockAvgWait', |
||||||
|
block_time: 'mockBlock_time', |
||||||
|
blockNum: 'mockBlockNum', |
||||||
|
fast: 30, |
||||||
|
fastest: 40, |
||||||
|
fastestWait: 'mockFastestWait', |
||||||
|
fastWait: 'mockFastWait', |
||||||
|
safeLow: 10, |
||||||
|
safeLowWait: 'mockSafeLowWait', |
||||||
|
speed: 'mockSpeed', |
||||||
|
} |
||||||
|
const mockPredictTableResponse = [ |
||||||
|
{ expectedTime: 400, expectedWait: 40, gasprice: 0.25, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 200, expectedWait: 20, gasprice: 0.5, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 100, expectedWait: 10, gasprice: 1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 75, expectedWait: 7.5, gasprice: 1.5, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 50, expectedWait: 5, gasprice: 2, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 35, expectedWait: 4.5, gasprice: 3, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 34, expectedWait: 4.4, gasprice: 3.1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 25, expectedWait: 4.2, gasprice: 3.5, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 20, expectedWait: 4, gasprice: 4, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 19, expectedWait: 3.9, gasprice: 4.1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 15, expectedWait: 3, gasprice: 7, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 14, expectedWait: 2.9, gasprice: 7.1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 12, expectedWait: 2.5, gasprice: 8, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 10, expectedWait: 2, gasprice: 10, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 9, expectedWait: 1.9, gasprice: 10.1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 5, expectedWait: 1, gasprice: 15, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 4, expectedWait: 0.9, gasprice: 15.1, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 2, expectedWait: 0.8, gasprice: 17, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 1.1, expectedWait: 0.6, gasprice: 19.9, somethingElse: 'foobar' }, |
||||||
|
{ expectedTime: 1, expectedWait: 0.5, gasprice: 20, somethingElse: 'foobar' }, |
||||||
|
] |
||||||
|
const fetchStub = sinon.stub().callsFake((url) => new Promise(resolve => { |
||||||
|
const dataToResolve = url.match(/ethgasAPI/) |
||||||
|
? mockEthGasApiResponse |
||||||
|
: mockPredictTableResponse |
||||||
|
resolve({ |
||||||
|
json: () => new Promise(resolve => resolve(dataToResolve)), |
||||||
|
}) |
||||||
|
})) |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
tempFetch = global.fetch |
||||||
|
tempDateNow = global.Date.now |
||||||
|
global.fetch = fetchStub |
||||||
|
global.Date.now = () => 2000000 |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
global.fetch = tempFetch |
||||||
|
global.Date.now = tempDateNow |
||||||
|
}) |
||||||
|
|
||||||
|
const mockState = { |
||||||
|
gas: { |
||||||
|
mockProp: 123, |
||||||
|
}, |
||||||
|
} |
||||||
|
const initState = { |
||||||
|
customData: { |
||||||
|
price: null, |
||||||
|
limit: '0x5208', |
||||||
|
}, |
||||||
|
basicEstimates: { |
||||||
|
average: null, |
||||||
|
fastestWait: null, |
||||||
|
fastWait: null, |
||||||
|
fast: null, |
||||||
|
safeLowWait: null, |
||||||
|
blockNum: null, |
||||||
|
avgWait: null, |
||||||
|
blockTime: null, |
||||||
|
speed: null, |
||||||
|
fastest: null, |
||||||
|
safeLow: null, |
||||||
|
}, |
||||||
|
basicEstimateIsLoading: true, |
||||||
|
errors: {}, |
||||||
|
gasEstimatesLoading: true, |
||||||
|
priceAndTimeEstimates: [], |
||||||
|
priceAndTimeEstimatesLastRetrieved: 0, |
||||||
|
basicPriceAndTimeEstimates: [], |
||||||
|
basicPriceAndTimeEstimatesLastRetrieved: 0, |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' |
||||||
|
const BASIC_GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_STARTED' |
||||||
|
const GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/GAS_ESTIMATE_LOADING_FINISHED' |
||||||
|
const GAS_ESTIMATE_LOADING_STARTED = 'metamask/gas/GAS_ESTIMATE_LOADING_STARTED' |
||||||
|
const RESET_CUSTOM_GAS_STATE = 'metamask/gas/RESET_CUSTOM_GAS_STATE' |
||||||
|
const SET_BASIC_GAS_ESTIMATE_DATA = 'metamask/gas/SET_BASIC_GAS_ESTIMATE_DATA' |
||||||
|
const SET_CUSTOM_GAS_ERRORS = 'metamask/gas/SET_CUSTOM_GAS_ERRORS' |
||||||
|
const SET_CUSTOM_GAS_LIMIT = 'metamask/gas/SET_CUSTOM_GAS_LIMIT' |
||||||
|
const SET_CUSTOM_GAS_PRICE = 'metamask/gas/SET_CUSTOM_GAS_PRICE' |
||||||
|
const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL' |
||||||
|
const SET_PRICE_AND_TIME_ESTIMATES = 'metamask/gas/SET_PRICE_AND_TIME_ESTIMATES' |
||||||
|
const SET_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_API_ESTIMATES_LAST_RETRIEVED' |
||||||
|
const SET_BASIC_API_ESTIMATES_LAST_RETRIEVED = 'metamask/gas/SET_BASIC_API_ESTIMATES_LAST_RETRIEVED' |
||||||
|
|
||||||
|
describe('GasReducer()', () => { |
||||||
|
it('should initialize state', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer({}), |
||||||
|
initState |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return state unchanged if it does not match a dispatched actions type', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: 'someOtherAction', |
||||||
|
value: 'someValue', |
||||||
|
}), |
||||||
|
Object.assign({}, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set basicEstimateIsLoading to true when receiving a BASIC_GAS_ESTIMATE_LOADING_STARTED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: BASIC_GAS_ESTIMATE_LOADING_STARTED, |
||||||
|
}), |
||||||
|
Object.assign({basicEstimateIsLoading: true}, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set basicEstimateIsLoading to false when receiving a BASIC_GAS_ESTIMATE_LOADING_FINISHED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: BASIC_GAS_ESTIMATE_LOADING_FINISHED, |
||||||
|
}), |
||||||
|
Object.assign({basicEstimateIsLoading: false}, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set gasEstimatesLoading to true when receiving a GAS_ESTIMATE_LOADING_STARTED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: GAS_ESTIMATE_LOADING_STARTED, |
||||||
|
}), |
||||||
|
Object.assign({gasEstimatesLoading: true}, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set gasEstimatesLoading to false when receiving a GAS_ESTIMATE_LOADING_FINISHED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: GAS_ESTIMATE_LOADING_FINISHED, |
||||||
|
}), |
||||||
|
Object.assign({gasEstimatesLoading: false}, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return a new object (and not just modify the existing state object)', () => { |
||||||
|
assert.deepEqual(GasReducer(mockState), mockState.gas) |
||||||
|
assert.notEqual(GasReducer(mockState), mockState.gas) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set basicEstimates when receiving a SET_BASIC_GAS_ESTIMATE_DATA action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_BASIC_GAS_ESTIMATE_DATA, |
||||||
|
value: { someProp: 'someData123' }, |
||||||
|
}), |
||||||
|
Object.assign({basicEstimates: {someProp: 'someData123'} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set priceAndTimeEstimates when receiving a SET_PRICE_AND_TIME_ESTIMATES action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_PRICE_AND_TIME_ESTIMATES, |
||||||
|
value: { someProp: 'someData123' }, |
||||||
|
}), |
||||||
|
Object.assign({priceAndTimeEstimates: {someProp: 'someData123'} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set customData.price when receiving a SET_CUSTOM_GAS_PRICE action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_CUSTOM_GAS_PRICE, |
||||||
|
value: 4321, |
||||||
|
}), |
||||||
|
Object.assign({customData: {price: 4321} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set customData.limit when receiving a SET_CUSTOM_GAS_LIMIT action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_CUSTOM_GAS_LIMIT, |
||||||
|
value: 9876, |
||||||
|
}), |
||||||
|
Object.assign({customData: {limit: 9876} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set customData.total when receiving a SET_CUSTOM_GAS_TOTAL action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_CUSTOM_GAS_TOTAL, |
||||||
|
value: 10000, |
||||||
|
}), |
||||||
|
Object.assign({customData: {total: 10000} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_API_ESTIMATES_LAST_RETRIEVED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_API_ESTIMATES_LAST_RETRIEVED, |
||||||
|
value: 1500000000000, |
||||||
|
}), |
||||||
|
Object.assign({ priceAndTimeEstimatesLastRetrieved: 1500000000000 }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_BASIC_API_ESTIMATES_LAST_RETRIEVED action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, |
||||||
|
value: 1700000000000, |
||||||
|
}), |
||||||
|
Object.assign({ basicPriceAndTimeEstimatesLastRetrieved: 1700000000000 }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should set errors when receiving a SET_CUSTOM_GAS_ERRORS action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: SET_CUSTOM_GAS_ERRORS, |
||||||
|
value: { someError: 'error_error' }, |
||||||
|
}), |
||||||
|
Object.assign({errors: {someError: 'error_error'} }, mockState.gas) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should return the initial state in response to a RESET_CUSTOM_GAS_STATE action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
GasReducer(mockState, { |
||||||
|
type: RESET_CUSTOM_GAS_STATE, |
||||||
|
}), |
||||||
|
Object.assign({}, initState) |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('basicGasEstimatesLoadingStarted', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
basicGasEstimatesLoadingStarted(), |
||||||
|
{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('basicGasEstimatesLoadingFinished', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
basicGasEstimatesLoadingFinished(), |
||||||
|
{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('fetchBasicGasAndTimeEstimates', () => { |
||||||
|
const mockDistpatch = sinon.spy() |
||||||
|
it('should call fetch with the expected params', async () => { |
||||||
|
await fetchBasicGasAndTimeEstimates()(mockDistpatch, () => ({ gas: Object.assign( |
||||||
|
{}, |
||||||
|
initState, |
||||||
|
{ basicPriceAndTimeEstimatesLastRetrieved: 1000000 } |
||||||
|
) })) |
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(0).args, |
||||||
|
[{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ] |
||||||
|
) |
||||||
|
assert.deepEqual( |
||||||
|
global.fetch.getCall(0).args, |
||||||
|
[ |
||||||
|
'https://ethgasstation.info/json/ethgasAPI.json', |
||||||
|
{ |
||||||
|
'headers': {}, |
||||||
|
'referrer': 'http://ethgasstation.info/json/', |
||||||
|
'referrerPolicy': 'no-referrer-when-downgrade', |
||||||
|
'body': null, |
||||||
|
'method': 'GET', |
||||||
|
'mode': 'cors', |
||||||
|
}, |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(1).args, |
||||||
|
[{ type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 } ] |
||||||
|
) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(2).args, |
||||||
|
[{ |
||||||
|
type: SET_BASIC_GAS_ESTIMATE_DATA, |
||||||
|
value: { |
||||||
|
average: 2, |
||||||
|
avgWait: 'mockAvgWait', |
||||||
|
blockTime: 'mockBlock_time', |
||||||
|
blockNum: 'mockBlockNum', |
||||||
|
fast: 3, |
||||||
|
fastest: 4, |
||||||
|
fastestWait: 'mockFastestWait', |
||||||
|
fastWait: 'mockFastWait', |
||||||
|
safeLow: 1, |
||||||
|
safeLowWait: 'mockSafeLowWait', |
||||||
|
speed: 'mockSpeed', |
||||||
|
}, |
||||||
|
}] |
||||||
|
) |
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(3).args, |
||||||
|
[{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }] |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('fetchGasEstimates', () => { |
||||||
|
const mockDistpatch = sinon.spy() |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
mockDistpatch.resetHistory() |
||||||
|
}) |
||||||
|
|
||||||
|
it('should call fetch with the expected params', async () => { |
||||||
|
global.fetch.resetHistory() |
||||||
|
await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign( |
||||||
|
{}, |
||||||
|
initState, |
||||||
|
{ priceAndTimeEstimatesLastRetrieved: 1000000 } |
||||||
|
) })) |
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(0).args, |
||||||
|
[{ type: GAS_ESTIMATE_LOADING_STARTED} ] |
||||||
|
) |
||||||
|
assert.deepEqual( |
||||||
|
global.fetch.getCall(0).args, |
||||||
|
[ |
||||||
|
'https://ethgasstation.info/json/predictTable.json', |
||||||
|
{ |
||||||
|
'headers': {}, |
||||||
|
'referrer': 'http://ethgasstation.info/json/', |
||||||
|
'referrerPolicy': 'no-referrer-when-downgrade', |
||||||
|
'body': null, |
||||||
|
'method': 'GET', |
||||||
|
'mode': 'cors', |
||||||
|
}, |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(1).args, |
||||||
|
[{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }] |
||||||
|
) |
||||||
|
|
||||||
|
const { type: thirdDispatchCallType, value: priceAndTimeEstimateResult } = mockDistpatch.getCall(2).args[0] |
||||||
|
assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES) |
||||||
|
assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2) |
||||||
|
assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100)) |
||||||
|
assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) |
||||||
|
assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(3).args, |
||||||
|
[{ type: GAS_ESTIMATE_LOADING_FINISHED }] |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
it('should not call fetch if the estimates were retrieved < 75000 ms ago', async () => { |
||||||
|
global.fetch.resetHistory() |
||||||
|
await fetchGasEstimates(5)(mockDistpatch, () => ({ gas: Object.assign( |
||||||
|
{}, |
||||||
|
initState, |
||||||
|
{ |
||||||
|
priceAndTimeEstimatesLastRetrieved: Date.now(), |
||||||
|
priceAndTimeEstimates: [{ |
||||||
|
expectedTime: '10', |
||||||
|
expectedWait: 2, |
||||||
|
gasprice: 50, |
||||||
|
}], |
||||||
|
} |
||||||
|
) })) |
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(0).args, |
||||||
|
[{ type: GAS_ESTIMATE_LOADING_STARTED} ] |
||||||
|
) |
||||||
|
assert.equal(global.fetch.callCount, 0) |
||||||
|
|
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(1).args, |
||||||
|
[{ |
||||||
|
type: SET_PRICE_AND_TIME_ESTIMATES, |
||||||
|
value: [ |
||||||
|
{ |
||||||
|
expectedTime: '10', |
||||||
|
expectedWait: 2, |
||||||
|
gasprice: 50, |
||||||
|
}, |
||||||
|
], |
||||||
|
|
||||||
|
}] |
||||||
|
) |
||||||
|
assert.deepEqual( |
||||||
|
mockDistpatch.getCall(2).args, |
||||||
|
[{ type: GAS_ESTIMATE_LOADING_FINISHED }] |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('gasEstimatesLoadingStarted', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
gasEstimatesLoadingStarted(), |
||||||
|
{ type: GAS_ESTIMATE_LOADING_STARTED } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('gasEstimatesLoadingFinished', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
gasEstimatesLoadingFinished(), |
||||||
|
{ type: GAS_ESTIMATE_LOADING_FINISHED } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setPricesAndTimeEstimates', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setPricesAndTimeEstimates('mockPricesAndTimeEstimates'), |
||||||
|
{ type: SET_PRICE_AND_TIME_ESTIMATES, value: 'mockPricesAndTimeEstimates' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setBasicGasEstimateData', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setBasicGasEstimateData('mockBasicEstimatData'), |
||||||
|
{ type: SET_BASIC_GAS_ESTIMATE_DATA, value: 'mockBasicEstimatData' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setCustomGasPrice', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setCustomGasPrice('mockCustomGasPrice'), |
||||||
|
{ type: SET_CUSTOM_GAS_PRICE, value: 'mockCustomGasPrice' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setCustomGasLimit', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setCustomGasLimit('mockCustomGasLimit'), |
||||||
|
{ type: SET_CUSTOM_GAS_LIMIT, value: 'mockCustomGasLimit' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setCustomGasTotal', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setCustomGasTotal('mockCustomGasTotal'), |
||||||
|
{ type: SET_CUSTOM_GAS_TOTAL, value: 'mockCustomGasTotal' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setCustomGasErrors', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setCustomGasErrors('mockErrorObject'), |
||||||
|
{ type: SET_CUSTOM_GAS_ERRORS, value: 'mockErrorObject' } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('setApiEstimatesLastRetrieved', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
setApiEstimatesLastRetrieved(1234), |
||||||
|
{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 1234 } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('resetCustomGasState', () => { |
||||||
|
it('should create the correct action', () => { |
||||||
|
assert.deepEqual( |
||||||
|
resetCustomGasState(), |
||||||
|
{ type: RESET_CUSTOM_GAS_STATE } |
||||||
|
) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
@ -0,0 +1,3 @@ |
|||||||
|
export function formatETHFee (ethFee) { |
||||||
|
return ethFee + ' ETH' |
||||||
|
} |
@ -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), |
||||||
|
}, |
||||||
|
] |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue