From f9aa5a70defdad3ce13a257dc689add1d4b7d736 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 30 Jul 2018 23:58:05 -0230 Subject: [PATCH 01/46] Adds new gas customization modal container (without content) --- app/_locales/en/messages.json | 18 ++ .../gas-modal-page-container.component.js | 79 +++++++++ .../gas-modal-page-container.container.js | 11 ++ .../gas-modal-page-container/index.js | 1 + .../gas-modal-page-container/index.scss | 50 ++++++ ...gas-modal-page-container-component.test.js | 156 ++++++++++++++++++ ...gas-modal-page-container-container.test.js | 44 +++++ ui/app/components/index.scss | 2 + ui/app/components/modals/modal.js | 36 +--- .../confirm-transaction-base.container.js | 2 +- ui/app/css/itcss/settings/variables.scss | 1 + 11 files changed, 371 insertions(+), 29 deletions(-) create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/index.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/index.scss create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 0fe8e81cd..6e7df3aa8 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -80,12 +80,18 @@ "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, + "advanced": { + "message": "Advanced" + }, "amount": { "message": "Amount" }, "amountPlusGas": { "message": "Amount + Gas" }, + "amountPlusTxFee": { + "message": "Amount + TX Fee" + }, "appDescription": { "message": "Ethereum Browser Extension", "description": "The description of the application" @@ -127,6 +133,9 @@ "balanceIsInsufficientGas": { "message": "Insufficient balance for current gas total" }, + "basic": { + "message": "Basic" + }, "beta": { "message": "BETA" }, @@ -303,6 +312,9 @@ "customGas": { "message": "Customize Gas" }, + "customGasSubTitle": { + "message": "Increasing fee may decrease processing times, but it is not guaranteed." + }, "customToken": { "message": "Custom Token" }, @@ -755,6 +767,9 @@ "optionalNickname": { "message": "Nickname (optional)" }, + "newTotal": { + "message": "New Total" + }, "next": { "message": "Next" }, @@ -820,6 +835,9 @@ "parameters": { "message": "Parameters" }, + "originalTotal": { + "message": "Original Total" + }, "password": { "message": "Password" }, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js new file mode 100644 index 000000000..dfc011c1e --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainer from '../../page-container' +import { Tabs, Tab } from '../../tabs' + +export default class GasModalPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + hideModal: PropTypes.func, + } + + state = {} + + renderBasicTabContent () { + return ( +
+ ) + } + + renderAdvancedTabContent () { + return ( +
+ ) + } + + renderInfoRow (className, totalLabelKey, fiatTotal, cryptoTotal) { + return ( +
+
+ {`${this.context.t(totalLabelKey)}:`}{fiatTotal} +
+
+ {`${this.context.t('amountPlusTxFee')}`}{cryptoTotal} +
+
+ ) + } + + renderTabContent (mainTabContent) { + return ( +
+ { mainTabContent() } + { this.renderInfoRow('info-row--fade', 'originalTotal', '$20.02 USD', '0.06685 ETH') } + { this.renderInfoRow('info-row', 'newTotal', '$20.02 USD', '0.06685 ETH') } +
+ ) + } + + renderTabs () { + return ( + + + { this.renderTabContent(this.renderBasicTabContent) } + + + { this.renderTabContent(this.renderAdvancedTabContent) } + + + ) + } + + render () { + const { hideModal } = this.props + + return ( + hideModal()} + onClose={() => hideModal()} + /> + ) + } +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js new file mode 100644 index 000000000..71c700507 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import GasModalPageContainer from './gas-modal-page-container.component' +import { hideModal } from '../../../actions' + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +export default connect(null, mapDispatchToProps)(GasModalPageContainer) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.js b/ui/app/components/gas-customization/gas-modal-page-container/index.js new file mode 100644 index 000000000..ec0ebad22 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.js @@ -0,0 +1 @@ +export { default } from './gas-modal-page-container.container' diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss new file mode 100644 index 000000000..2d2250212 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -0,0 +1,50 @@ +.gas-modal-content { + .basic-tab { + height: 219px; + } + + .advanced-tab { + height: 475px; + } + + .info-row, .info-row--fade { + width: 100%; + background: $polar; + padding: 15px 21px; + display: flex; + flex-flow: column; + color: $scorpion; + + .total-info, .sum-info { + display: flex; + flex-flow: row; + justify-content: space-between; + } + + .total-info { + .total-label { + font-size: 16px; + } + + .total-value { + font-size: 16px; + font-weight: bold; + } + } + + .sum-info { + .sum-label { + font-size: 12px; + } + + .sum-value { + font-size: 14px; + } + } + } + + .info-row--fade { + background: white; + color: $dusty-gray; + } +} \ No newline at end of file diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js new file mode 100644 index 000000000..ffe242de2 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -0,0 +1,156 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import GasModalPageContainer from '../gas-modal-page-container.component.js' + +import PageContainer from '../../../page-container' +import { Tab } from '../../../tabs' + +const propsMethodSpies = { + hideModal: sinon.spy(), +} + +describe('GasModalPageContainer Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.hideModal.resetHistory() + }) + + 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, 'customGasSpeedUp') + 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.hideModal.callCount, 0) + onCancel() + assert.equal(propsMethodSpies.hideModal.callCount, 1) + onClose() + assert.equal(propsMethodSpies.hideModal.callCount, 2) + }) + + it('should pass the correct renderTabs property to PageContainer', () => { + sinon.stub(GasModalPageContainer.prototype, 'renderTabs').returns('mockTabs') + const renderTabsWrapperTester = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props() + assert.equal(tabsComponent, 'mockTabs') + GasModalPageContainer.prototype.renderTabs.restore() + }) + }) + + describe('renderTabs', () => { + it('should render a Tabs component with "Basic" and "Advanced" tabs', () => { + const renderTabsResult = wrapper.instance().renderTabs() + 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 render the expected children of each tab', () => { + const GP = GasModalPageContainer.prototype + sinon.spy(GP, 'renderTabContent') + assert.equal(GP.renderTabContent.callCount, 0) + + wrapper.instance().renderTabs() + + assert.equal(GP.renderTabContent.callCount, 2) + + assert.deepEqual(GP.renderTabContent.firstCall.args, [wrapper.instance().renderBasicTabContent]) + assert.deepEqual(GP.renderTabContent.secondCall.args, [wrapper.instance().renderAdvancedTabContent]) + + GP.renderTabContent.restore() + }) + }) + + describe('renderTabContent', () => { + it('should render a root gas-modal-content div', () => { + const renderTabContentResult = wrapper.instance().renderTabContent(() => {}) + const renderedTabContent = shallow(renderTabContentResult) + assert.equal(renderedTabContent.props().className, 'gas-modal-content') + }) + + it('should render the element returned by the passed func as its first child', () => { + const renderTabContentResult = wrapper.instance().renderTabContent(() => Mock content) + const renderedTabContent = shallow(renderTabContentResult) + assert(renderedTabContent.childAt(0).equals(Mock content)) + }) + + it('should render the element results of renderInfoRow calls as second and third childs', () => { + const GP = GasModalPageContainer.prototype + sinon.stub(GP, 'renderInfoRow').callsFake((...args) => args.join(',')) + + const renderTabContentResult = wrapper.instance().renderTabContent(() => Mock content) + const renderedTabContent = shallow(renderTabContentResult) + assert.equal(renderedTabContent.childAt(1).text(), 'info-row--fade,originalTotal,$20.02 USD,0.06685 ETH') + assert.equal(renderedTabContent.childAt(2).text(), 'info-row,newTotal,$20.02 USD,0.06685 ETH') + + GP.renderInfoRow.restore() + }) + }) + + describe('renderInfoRow', () => { + it('should render a div with the passed className and two children, each with the expected text', () => { + const renderInfoRowResult = wrapper.instance().renderInfoRow('mockClassName', 'mockLabelKey', 'mockFiatAmount', 'mockCryptoAmount') + const renderedInfoRow = shallow(renderInfoRowResult) + assert.equal(renderedInfoRow.props().className, 'mockClassName') + + const firstChild = renderedInfoRow.childAt(0) + const secondhild = renderedInfoRow.childAt(1) + + assert.equal(firstChild.props().className, 'total-info') + assert.equal(secondhild.props().className, 'sum-info') + + assert.equal(firstChild.childAt(0).text(), 'mockLabelKey:') + assert.equal(firstChild.childAt(1).text(), 'mockFiatAmount') + assert.equal(secondhild.childAt(0).text(), 'amountPlusTxFee') + assert.equal(secondhild.childAt(1).text(), 'mockCryptoAmount') + }) + }) + + describe('renderBasicTabContent', () => { + it('should render', () => { + const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent() + const renderedBasicTabContent = shallow(renderBasicTabContentResult) + assert.equal(renderedBasicTabContent.props().className, 'basic-tab') + }) + }) + + describe('renderAdvancedTabContent', () => { + it('should render', () => { + const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent() + const renderedAdvancedTabContent = shallow(renderAdvancedTabContentResult) + assert.equal(renderedAdvancedTabContent.props().className, 'advanced-tab') + }) + }) +}) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js new file mode 100644 index 000000000..5b133fbe2 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -0,0 +1,44 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +// let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + hideModal: sinon.spy(), +} + +proxyquire('../gas-modal-page-container.container.js', { + 'react-redux': { + connect: (ms, md) => { + // mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../actions': actionSpies, +}) + +describe('gas-modal-page-container container', () => { + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('hideModal()', () => { + it('should dispatch a hideModal action', () => { + mapDispatchToPropsObject.hideModal() + assert(dispatchSpy.calledOnce) + assert(actionSpies.hideModal.calledOnce) + }) + }) + + }) + +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index f901aed7d..48c6496d9 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -63,3 +63,5 @@ @import './sidebars/index'; @import './unit-input/index'; + +@import './gas-customization/gas-modal-page-container/index' diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 5aff4f5e1..46347312c 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -17,18 +17,17 @@ const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') -const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const QRScanner = require('./qr-scanner') import ConfirmRemoveAccount from './confirm-remove-account' import ConfirmResetAccount from './confirm-reset-account' import TransactionConfirmed from './transaction-confirmed' -import ConfirmCustomizeGasModal from './customize-gas' import CancelTransaction from './cancel-transaction' import WelcomeBeta from './welcome-beta' import RejectTransactions from './reject-transactions' import ClearApprovedOrigins from './clear-approved-origins' +import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -295,7 +294,7 @@ const MODALS = { CUSTOMIZE_GAS: { contents: [ - h(CustomizeGasModal), + h(ConfirmCustomizeGasModal), ], mobileModalStyle: { width: '100vw', @@ -307,35 +306,16 @@ const MODALS = { margin: '0 auto', }, laptopModalStyle: { - width: '720px', - height: '377px', + width: 'auto', + height: '0px', top: '80px', + left: '0px', transform: 'none', - left: '0', - right: '0', margin: '0 auto', + position: 'relative', }, - }, - - CONFIRM_CUSTOMIZE_GAS: { - contents: h(ConfirmCustomizeGasModal), - mobileModalStyle: { - width: '100vw', - height: '100vh', - top: '0', - transform: 'none', - left: '0', - right: '0', - margin: '0 auto', - }, - laptopModalStyle: { - width: '720px', - height: '377px', - top: '80px', - transform: 'none', - left: '0', - right: '0', - margin: '0 auto', + contentStyle: { + borderRadius: '8px', }, }, diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index 626143ac7..a3ec82863 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -117,7 +117,7 @@ const mapDispatchToProps = dispatch => { return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit })) }, showCustomizeGasModal: ({ txData, onSubmit, validate }) => { - return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate })) + return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate })) }, updateGasAndCalculate: ({ gasLimit, gasPrice }) => { return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index f90c8edc3..6c2b82f39 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -56,6 +56,7 @@ $zumthor: #edf7ff; $ecstasy: #f7861c; $linen: #fdf4f4; $oslo-gray: #8C8E94; +$polar: #fafcfe; /* Z-Indicies From 88d8eb289edd7bec86993aac946ac85e4e5219ec Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 6 Aug 2018 11:47:58 -0230 Subject: [PATCH 02/46] Use correct message key in gas-modal-page-container.component.js --- .../gas-modal-page-container.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index dfc011c1e..51a202df1 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -68,7 +68,7 @@ export default class GasModalPageContainer extends Component { return ( hideModal()} From 5e7409482b6fc55eafc330e4bc119f7485f068bb Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 6 Aug 2018 12:04:06 -0230 Subject: [PATCH 03/46] Use BEM for css in gas-modal-page-container --- .../gas-modal-page-container.component.js | 18 ++++++++------- .../gas-modal-page-container/index.scss | 22 +++++++++---------- ...gas-modal-page-container-component.test.js | 14 ++++++------ 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 51a202df1..697594ec9 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -16,24 +16,26 @@ export default class GasModalPageContainer extends Component { renderBasicTabContent () { return ( -
+
) } renderAdvancedTabContent () { return ( -
+
) } renderInfoRow (className, totalLabelKey, fiatTotal, cryptoTotal) { return (
-
- {`${this.context.t(totalLabelKey)}:`}{fiatTotal} +
+ {`${this.context.t(totalLabelKey)}:`} + {fiatTotal}
-
- {`${this.context.t('amountPlusTxFee')}`}{cryptoTotal} +
+ {`${this.context.t('amountPlusTxFee')}`} + {cryptoTotal}
) @@ -43,8 +45,8 @@ export default class GasModalPageContainer extends Component { return (
{ mainTabContent() } - { this.renderInfoRow('info-row--fade', 'originalTotal', '$20.02 USD', '0.06685 ETH') } - { this.renderInfoRow('info-row', 'newTotal', '$20.02 USD', '0.06685 ETH') } + { this.renderInfoRow('gas-modal-content__info-row--fade', 'originalTotal', '$20.02 USD', '0.06685 ETH') } + { this.renderInfoRow('gas-modal-content__info-row', 'newTotal', '$20.02 USD', '0.06685 ETH') }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 2d2250212..a6609a385 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -1,13 +1,13 @@ .gas-modal-content { - .basic-tab { + &__basic-tab { height: 219px; } - .advanced-tab { + &__advanced-tab { height: 475px; } - .info-row, .info-row--fade { + &__info-row, &__info-row--fade { width: 100%; background: $polar; padding: 15px 21px; @@ -15,35 +15,35 @@ flex-flow: column; color: $scorpion; - .total-info, .sum-info { + &__total-info, &__sum-info { display: flex; flex-flow: row; justify-content: space-between; } - .total-info { - .total-label { + &__total-info { + &__total-label { font-size: 16px; } - .total-value { + &__total-value { font-size: 16px; font-weight: bold; } } - .sum-info { - .sum-label { + &__sum-info { + &__sum-label { font-size: 12px; } - .sum-value { + &__sum-value { font-size: 14px; } } } - .info-row--fade { + &__info-row--fade { background: white; color: $dusty-gray; } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index ffe242de2..32f644765 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -36,7 +36,7 @@ describe('GasModalPageContainer Component', function () { disabled, } = wrapper.find(PageContainer).props() assert.equal(title, 'customGas') - assert.equal(subtitle, 'customGasSpeedUp') + assert.equal(subtitle, 'customGasSubTitle') assert.equal(disabled, false) }) @@ -112,8 +112,8 @@ describe('GasModalPageContainer Component', function () { const renderTabContentResult = wrapper.instance().renderTabContent(() => Mock content) const renderedTabContent = shallow(renderTabContentResult) - assert.equal(renderedTabContent.childAt(1).text(), 'info-row--fade,originalTotal,$20.02 USD,0.06685 ETH') - assert.equal(renderedTabContent.childAt(2).text(), 'info-row,newTotal,$20.02 USD,0.06685 ETH') + assert.equal(renderedTabContent.childAt(1).text(), 'gas-modal-content__info-row--fade,originalTotal,$20.02 USD,0.06685 ETH') + assert.equal(renderedTabContent.childAt(2).text(), 'gas-modal-content__info-row,newTotal,$20.02 USD,0.06685 ETH') GP.renderInfoRow.restore() }) @@ -128,8 +128,8 @@ describe('GasModalPageContainer Component', function () { const firstChild = renderedInfoRow.childAt(0) const secondhild = renderedInfoRow.childAt(1) - assert.equal(firstChild.props().className, 'total-info') - assert.equal(secondhild.props().className, 'sum-info') + assert.equal(firstChild.props().className, 'mockClassName__total-info') + assert.equal(secondhild.props().className, 'mockClassName__sum-info') assert.equal(firstChild.childAt(0).text(), 'mockLabelKey:') assert.equal(firstChild.childAt(1).text(), 'mockFiatAmount') @@ -142,7 +142,7 @@ describe('GasModalPageContainer Component', function () { it('should render', () => { const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent() const renderedBasicTabContent = shallow(renderBasicTabContentResult) - assert.equal(renderedBasicTabContent.props().className, 'basic-tab') + assert.equal(renderedBasicTabContent.props().className, 'gas-modal-content__basic-tab') }) }) @@ -150,7 +150,7 @@ describe('GasModalPageContainer Component', function () { it('should render', () => { const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent() const renderedAdvancedTabContent = shallow(renderAdvancedTabContentResult) - assert.equal(renderedAdvancedTabContent.props().className, 'advanced-tab') + assert.equal(renderedAdvancedTabContent.props().className, 'gas-modal-content__advanced-tab') }) }) }) From 342dc95410b10f042b3f8ee4135f5fef1fd6fe93 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 2 Aug 2018 13:32:22 -0230 Subject: [PATCH 04/46] Adds the content of the advanced tab - w/o chart or dynamic content - to gas customize modal. --- app/_locales/en/messages.json | 6 + .../advanced-tab-content.component.js | 105 +++++++++++++++++ .../advanced-tab-content/index.js | 1 + .../advanced-tab-content/index.scss | 109 ++++++++++++++++++ .../time-remaining/index.js | 1 + .../time-remaining/index.scss | 13 +++ .../time-remaining.component.js | 33 ++++++ .../time-remaining/time-remaining.utils.js | 11 ++ .../gas-modal-page-container.component.js | 40 +++++-- .../gas-modal-page-container.container.js | 19 ++- .../gas-modal-page-container/index.scss | 8 ++ .../gas-slider/gas-slider.component.js | 48 ++++++++ .../gas-customization/gas-slider/index.js | 1 + .../gas-customization/gas-slider/index.scss | 54 +++++++++ .../components/gas-customization/index.scss | 3 + ui/app/components/index.scss | 8 +- ui/app/css/itcss/components/gas-slider.scss | 51 -------- ui/app/css/itcss/settings/variables.scss | 1 + ui/app/ducks/custom-gas.js | 67 +++++++++++ ui/app/reducers.js | 3 + ui/app/selectors/custom-gas.js | 19 +++ 21 files changed, 538 insertions(+), 63 deletions(-) create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js create mode 100644 ui/app/components/gas-customization/gas-slider/gas-slider.component.js create mode 100644 ui/app/components/gas-customization/gas-slider/index.js create mode 100644 ui/app/components/gas-customization/gas-slider/index.scss create mode 100644 ui/app/components/gas-customization/index.scss create mode 100644 ui/app/ducks/custom-gas.js create mode 100644 ui/app/selectors/custom-gas.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6e7df3aa8..103d8534f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -701,6 +701,9 @@ "missingYourTokens": { "message": "Don't see your tokens?" }, + "minutesShorthand": { + "message": "Min" + }, "myAccounts": { "message": "My Accounts" }, @@ -1036,6 +1039,9 @@ "secretPhrase": { "message": "Enter your secret twelve word phrase here to restore your vault." }, + "secondsShorthand": { + "message": "Sec" + }, "seedPhraseReq": { "message": "Seed phrases are 12 words long" }, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js new file mode 100644 index 000000000..7ddf13e51 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + MIN_GAS_PRICE_DEC, + MIN_GAS_LIMIT_DEC, +} from '../../../send/send.constants' +import GasSlider from '../../gas-slider' +import TimeRemaining from './time-remaining' + +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, + millisecondsRemaining: PropTypes.number, + } + + gasInput (value, onChange, min, precision, showGWEI) { + return ( +
+ onChange(Number(event.target.value))} + /> + {showGWEI + ? GWEI + : null} +
+ ) + } + + infoButton (onClick) { + return + } + + render () { + const { + updateCustomGasPrice, + updateCustomGasLimit, + millisecondsRemaining, + customGasPrice, + customGasLimit, + } = this.props + + return ( +
+
+
+ New Transaction Fee + ~Transaction Time +
+
+
+ $0.30 +
+ +
+
+
+ Live Transaction Fee Predictions +
+
+
+ { + updateCustomGasPrice(Number(value)) + }} + lowLabel={'Cheaper'} + highLabel={'Faster'} + value={customGasPrice} + step={0.1} + max={200} + min={0} + coloredStart={{}} + /> +
+
+
+
+ Gas Price + { this.infoButton(() => {}) } +
+ { this.gasInput(customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } +
+
+
+ Gas Limit + { this.infoButton(() => {}) } +
+ { this.gasInput(customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) } +
+
+
+ ) + } +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js new file mode 100644 index 000000000..492037f25 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.js @@ -0,0 +1 @@ +export { default } from './advanced-tab-content.component' diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss new file mode 100644 index 000000000..5dc30e061 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -0,0 +1,109 @@ +@import './time-remaining/index'; + +.advanced-tab { + display: flex; + flex-flow: column; + border-bottom: 1px solid $alto; + + &__transaction-data-summary, + &__fee-chart-title, + &__gas-edit-row { + padding-left: 24px; + padding-right: 24px; + } + + &__transaction-data-summary { + display: flex; + flex-flow: column; + color: $mid-gray; + margin-top: 12px; + + &__titles, + &__container { + display: flex; + flex-flow: row; + justify-content: space-between; + font-size: 12px; + } + + &__container { + font-size: 26px; + margin-top: 6px; + } + } + + &__fee-chart-title { + font-size: 14px; + color: $scorpion; + margin-top: 22px; + } + + &__fee-chart { + padding-left: 10px; + margin-top: 24px; + height: 134px; + } + + &__slider-container { + padding-left: 27px; + padding-right: 27px; + } + + &__gas-edit-rows { + margin-top: 44px; + height: 87px; + display: flex; + flex-flow: column; + justify-content: space-between; + } + + &__gas-edit-row { + display: flex; + flex-flow: row; + justify-content: space-between; + + &__label { + color: $mid-gray; + font-size: 16px; + + .info-circle { + color: $silver; + margin-left: 10px; + } + } + + + &__input-wrapper { + position: relative; + } + + &__input { + border: 1px solid $dusty-gray; + border-radius: 4px; + color: $mid-gray; + font-size: 16px; + height: 37px; + width: 163px; + padding: 8px 10px 10px 10px; + } + + 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; + } + } +} \ No newline at end of file diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js new file mode 100644 index 000000000..61b681e1a --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js @@ -0,0 +1 @@ +export { default } from './time-remaining.component' diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss new file mode 100644 index 000000000..01bb06268 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss @@ -0,0 +1,13 @@ +.time-remaining { + .minutes-num, .seconds-num { + font-size: 26px; + } + + .seconds-num { + margin-left: 7px; + } + + .minutes-label, .seconds-label { + font-size: 14px; + } +} \ No newline at end of file diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js new file mode 100644 index 000000000..826d41f9c --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js @@ -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 ( +
+ {minutes} + {this.context.t('minutesShorthand')} + {seconds} + {this.context.t('secondsShorthand')} +
+ ) + } +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js new file mode 100644 index 000000000..cf43e0acb --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js @@ -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, +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 697594ec9..1d7a9d986 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -2,6 +2,7 @@ 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' export default class GasModalPageContainer extends Component { static contextTypes = { @@ -10,6 +11,10 @@ export default class GasModalPageContainer extends Component { static propTypes = { hideModal: PropTypes.func, + updateCustomGasPrice: PropTypes.func, + updateCustomGasLimit: PropTypes.func, + customGasPrice: PropTypes.number, + customGasLimit: PropTypes.number, } state = {} @@ -20,9 +25,22 @@ export default class GasModalPageContainer extends Component { ) } - renderAdvancedTabContent () { + renderAdvancedTabContent = () => { + const { + updateCustomGasPrice, + updateCustomGasLimit, + customGasPrice, + customGasLimit, + } = this.props + return ( -
+ ) } @@ -68,14 +86,16 @@ export default class GasModalPageContainer extends Component { const { hideModal } = this.props return ( - hideModal()} - onClose={() => hideModal()} - /> +
+ hideModal()} + onClose={() => hideModal()} + /> +
) } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 71c700507..f7ac91a38 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -1,11 +1,28 @@ import { connect } from 'react-redux' import GasModalPageContainer from './gas-modal-page-container.component' import { hideModal } from '../../../actions' +import { + setCustomGasPrice, + setCustomGasLimit, +} from '../../../ducks/custom-gas' +import { + getCustomGasPrice, + getCustomGasLimit, +} from '../../../selectors/custom-gas' + +const mapStateToProps = state => { + return { + customGasPrice: getCustomGasPrice(state), + customGasLimit: getCustomGasLimit(state), + } +} const mapDispatchToProps = dispatch => { return { hideModal: () => dispatch(hideModal()), + updateCustomGasPrice: (newPrice) => dispatch(setCustomGasPrice(newPrice)), + updateCustomGasLimit: (newLimit) => dispatch(setCustomGasLimit(newLimit)), } } -export default connect(null, mapDispatchToProps)(GasModalPageContainer) +export default connect(mapStateToProps, mapDispatchToProps)(GasModalPageContainer) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index a6609a385..027165b48 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -1,3 +1,11 @@ +@import './advanced-tab-content/index'; + +.gas-modal-page-container { + .page-container { + width: 391px; + } +} + .gas-modal-content { &__basic-tab { height: 219px; diff --git a/ui/app/components/gas-customization/gas-slider/gas-slider.component.js b/ui/app/components/gas-customization/gas-slider/gas-slider.component.js new file mode 100644 index 000000000..5836e7dfc --- /dev/null +++ b/ui/app/components/gas-customization/gas-slider/gas-slider.component.js @@ -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 ( +
+ onChange(event.target.value)} + /> +
+
+
+
+ {lowLabel} + {highLabel} +
+
+ ) + } +} diff --git a/ui/app/components/gas-customization/gas-slider/index.js b/ui/app/components/gas-customization/gas-slider/index.js new file mode 100644 index 000000000..f1752c93f --- /dev/null +++ b/ui/app/components/gas-customization/gas-slider/index.js @@ -0,0 +1 @@ +export { default } from './gas-slider.component' diff --git a/ui/app/components/gas-customization/gas-slider/index.scss b/ui/app/components/gas-customization/gas-slider/index.scss new file mode 100644 index 000000000..e6c734367 --- /dev/null +++ b/ui/app/components/gas-customization/gas-slider/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/ui/app/components/gas-customization/index.scss b/ui/app/components/gas-customization/index.scss new file mode 100644 index 000000000..91ca5004e --- /dev/null +++ b/ui/app/components/gas-customization/index.scss @@ -0,0 +1,3 @@ +@import './gas-slider/index'; + +@import './gas-modal-page-container/index'; diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 48c6496d9..156b1b9f6 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -64,4 +64,10 @@ @import './unit-input/index'; -@import './gas-customization/gas-modal-page-container/index' +@import './gas-customization/gas-modal-page-container/index'; + +@import './gas-customization/gas-modal-page-container/index'; + +@import './gas-customization/gas-modal-page-container/index'; + +@import './gas-customization/index'; diff --git a/ui/app/css/itcss/components/gas-slider.scss b/ui/app/css/itcss/components/gas-slider.scss index c27a560bd..e69de29bb 100644 --- a/ui/app/css/itcss/components/gas-slider.scss +++ b/ui/app/css/itcss/components/gas-slider.scss @@ -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; - } -} \ No newline at end of file diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 6c2b82f39..1c95f06a0 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -57,6 +57,7 @@ $ecstasy: #f7861c; $linen: #fdf4f4; $oslo-gray: #8C8E94; $polar: #fafcfe; +$blizzard-blue: #bfdef3; /* Z-Indicies diff --git a/ui/app/ducks/custom-gas.js b/ui/app/ducks/custom-gas.js new file mode 100644 index 000000000..f1f483e93 --- /dev/null +++ b/ui/app/ducks/custom-gas.js @@ -0,0 +1,67 @@ +import extend from 'xtend' + +// Actions +const SET_CUSTOM_GAS_PRICE = 'metamask/custom-gas/SET_CUSTOM_GAS_PRICE' +const SET_CUSTOM_GAS_LIMIT = 'metamask/custom-gas/SET_CUSTOM_GAS_LIMIT' +const SET_CUSTOM_GAS_ERRORS = 'metamask/custom-gas/SET_CUSTOM_GAS_ERRORS' +const RESET_CUSTOM_GAS_STATE = 'metamask/custom-gas/RESET_CUSTOM_GAS_STATE' + +// TODO: determine if this approach to initState is consistent with conventional ducks pattern +const initState = { + price: 0, + limit: 21000, + errors: {}, +} + +// Reducer +export default function reducer ({ customGas: customGasState = initState }, action = {}) { + const newState = extend({}, customGasState) + + switch (action.type) { + case SET_CUSTOM_GAS_PRICE: + return extend(newState, { + price: action.value, + }) + case SET_CUSTOM_GAS_LIMIT: + return extend(newState, { + limit: action.value, + }) + case SET_CUSTOM_GAS_ERRORS: + return extend(newState, { + errors: { + ...newState.errors, + ...action.value, + }, + }) + case RESET_CUSTOM_GAS_STATE: + return extend({}, initState) + default: + return newState + } +} + +// Action Creators +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 setCustomGasErrors (newErrors) { + return { + type: SET_CUSTOM_GAS_ERRORS, + value: newErrors, + } +} + +export function resetCustomGasState () { + return { type: RESET_CUSTOM_GAS_STATE } +} diff --git a/ui/app/reducers.js b/ui/app/reducers.js index e1a982f93..7234c3c02 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -10,6 +10,7 @@ const reduceApp = require('./reducers/app') const reduceLocale = require('./reducers/locale') const reduceSend = require('./ducks/send.duck').default import reduceConfirmTransaction from './ducks/confirm-transaction.duck' +import reduceCustomGas from './ducks/custom-gas' window.METAMASK_CACHED_LOG_STATE = null @@ -49,6 +50,8 @@ function rootReducer (state, action) { state.confirmTransaction = reduceConfirmTransaction(state, action) + state.customGas = reduceCustomGas(state, action) + window.METAMASK_CACHED_LOG_STATE = state return state } diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js new file mode 100644 index 000000000..97280190f --- /dev/null +++ b/ui/app/selectors/custom-gas.js @@ -0,0 +1,19 @@ +const selectors = { + getCustomGasPrice, + getCustomGasLimit, + getCustomGasErrors, +} + +module.exports = selectors + +function getCustomGasPrice (state) { + return state.customGas.price +} + +function getCustomGasLimit (state) { + return state.customGas.limit +} + +function getCustomGasErrors (state) { + return state.customGas.errors +} From d55a2615a4af1a0e4d589d0b82904f46cfbff6f7 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 7 Aug 2018 12:27:42 -0230 Subject: [PATCH 05/46] Split advanced-tab-content.component.js render() method into smaller pieces; add translations to the same file. --- app/_locales/en/messages.json | 12 +++ .../advanced-tab-content.component.js | 81 ++++++++++++------- .../gas-modal-page-container.component.js | 1 + 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 103d8534f..017a8a380 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -439,6 +439,9 @@ "failed": { "message": "Failed" }, + "feeChartTitle": { + "message": "Live Transaction Fee Predictions" + }, "fiat": { "message": "Fiat", "description": "Exchange type" @@ -493,6 +496,9 @@ "gasPrice": { "message": "Gas Price (GWEI)" }, + "gasPriceNoDenom": { + "message": "Gas Price" + }, "gasPriceCalculation": { "message": "We calculate the suggested gas prices based on network success rates." }, @@ -773,6 +779,9 @@ "newTotal": { "message": "New Total" }, + "newTransactionFee": { + "message": "New Transaction Fee" + }, "next": { "message": "Next" }, @@ -1274,6 +1283,9 @@ "transactionNumber": { "message": "Transaction Number" }, + "transactionTime": { + "message": "Transaction Time" + }, "transfer": { "message": "Transfer" }, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 7ddf13e51..8e593f029 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -18,6 +18,7 @@ export default class AdvancedTabContent extends Component { customGasPrice: PropTypes.number, customGasLimit: PropTypes.number, millisecondsRemaining: PropTypes.number, + totalFee: PropTypes.string, } gasInput (value, onChange, min, precision, showGWEI) { @@ -40,6 +41,46 @@ export default class AdvancedTabContent extends Component { return } + renderDataSummary (totalFee, millisecondsRemaining) { + return ( +
+
+ { this.context.t('newTransactionFee') } + ~{ this.context.t('transactionTime') } +
+
+
+ {totalFee} +
+ +
+
+ ) + } + + renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit) { + return ( +
+
+
+ { this.context.t('gasPriceNoDenom') } + { this.infoButton(() => {}) } +
+ { this.gasInput(customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } +
+
+
+ { this.context.t('gasLimit') } + { this.infoButton(() => {}) } +
+ { this.gasInput(customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) } +
+
+ ) + } + render () { const { updateCustomGasPrice, @@ -47,26 +88,14 @@ export default class AdvancedTabContent extends Component { millisecondsRemaining, customGasPrice, customGasLimit, + totalFee, } = this.props return (
-
-
- New Transaction Fee - ~Transaction Time -
-
-
- $0.30 -
- -
-
+ { this.renderDataSummary(totalFee, millisecondsRemaining) }
- Live Transaction Fee Predictions + { this.context.t('feeChartTitle') }
@@ -83,22 +112,12 @@ export default class AdvancedTabContent extends Component { coloredStart={{}} />
-
-
-
- Gas Price - { this.infoButton(() => {}) } -
- { this.gasInput(customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } -
-
-
- Gas Limit - { this.infoButton(() => {}) } -
- { this.gasInput(customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) } -
-
+ { this.renderGasEditRows( + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit + ) }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 1d7a9d986..e47ebaf65 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -40,6 +40,7 @@ export default class GasModalPageContainer extends Component { customGasPrice={customGasPrice} customGasLimit={customGasLimit} millisecondsRemaining={91000} + totalFee={'$0.30'} /> ) } From 3b9ec8e1bc8f3db9cfd645b10e4908f06096c70c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 7 Aug 2018 12:29:14 -0230 Subject: [PATCH 06/46] Remove gas slider from advance-tab-content.component --- .../advanced-tab-content.component.js | 15 --------------- .../advanced-tab-content/index.scss | 1 + .../gas-modal-page-container/index.scss | 5 +---- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 8e593f029..a09869a46 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -4,7 +4,6 @@ import { MIN_GAS_PRICE_DEC, MIN_GAS_LIMIT_DEC, } from '../../../send/send.constants' -import GasSlider from '../../gas-slider' import TimeRemaining from './time-remaining' export default class AdvancedTabContent extends Component { @@ -98,20 +97,6 @@ export default class AdvancedTabContent extends Component { { this.context.t('feeChartTitle') }
-
- { - updateCustomGasPrice(Number(value)) - }} - lowLabel={'Cheaper'} - highLabel={'Faster'} - value={customGasPrice} - step={0.1} - max={200} - min={0} - coloredStart={{}} - /> -
{ this.renderGasEditRows( customGasPrice, updateCustomGasPrice, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index 5dc30e061..0c95afc48 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -4,6 +4,7 @@ display: flex; flex-flow: column; border-bottom: 1px solid $alto; + height: 430px; &__transaction-data-summary, &__fee-chart-title, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 027165b48..ff512537e 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -11,11 +11,8 @@ height: 219px; } - &__advanced-tab { - height: 475px; - } - &__info-row, &__info-row--fade { + .info-row, .info-row--fade { width: 100%; background: $polar; padding: 15px 21px; From 99c8804eeb8c3e407fa9f2d806c145113ec6ec2c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 8 Aug 2018 11:21:06 -0230 Subject: [PATCH 07/46] Add tests for advanced-tab-component.js and subcomponents. --- .../advanced-tab-content.component.js | 30 +-- .../advanced-tab-content-component.test.js | 215 ++++++++++++++++++ .../tests/time-remaining-component.test.js | 30 +++ ...gas-modal-page-container-component.test.js | 18 +- ui/lib/shallow-with-context.js | 7 + 5 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js create mode 100644 ui/lib/shallow-with-context.js diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index a09869a46..69cd06cde 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -27,6 +27,8 @@ export default class AdvancedTabContent extends Component { className="advanced-tab__gas-edit-row__input" type="number" value={value} + min={min} + precision={precision} onChange={event => onChange(Number(event.target.value))} /> {showGWEI @@ -59,23 +61,23 @@ export default class AdvancedTabContent extends Component { ) } + renderGasEditRow (labelKey, ...gasInputArgs) { + return ( +
+
+ { this.context.t(labelKey) } + { this.infoButton(() => {}) } +
+ { this.gasInput(...gasInputArgs) } +
+ ) + } + renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit) { return (
-
-
- { this.context.t('gasPriceNoDenom') } - { this.infoButton(() => {}) } -
- { this.gasInput(customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } -
-
-
- { this.context.t('gasLimit') } - { this.infoButton(() => {}) } -
- { this.gasInput(customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) } -
+ { this.renderGasEditRow('gasPriceNoDenom', customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } + { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js new file mode 100644 index 000000000..3efe1d2a9 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -0,0 +1,215 @@ +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 TimeRemaining from '../time-remaining' + +const propsMethodSpies = { + updateCustomGasPrice: sinon.spy(), + updateCustomGasLimit: sinon.spy(), +} + +sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow') +sinon.spy(AdvancedTabContent.prototype, 'gasInput') + +describe('AdvancedTabContent Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.updateCustomGasPrice.resetHistory() + propsMethodSpies.updateCustomGasLimit.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, 4) + + assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) + assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart-title')) + assert(advancedTabChildren.at(2).hasClass('advanced-tab__fee-chart')) + assert(advancedTabChildren.at(3).hasClass('advanced-tab__gas-edit-rows')) + }) + }) + + 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).is(TimeRemaining)) + assert.equal(dataNode.children().at(1).props().milliseconds, '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('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 + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasEditRows = shallow(wrapper.instance().renderGasEditRows( + 'mockGasPrice', + () => 'mockUpdateCustomGasPriceReturn', + 'mockGasLimit', + () => 'mockUpdateCustomGasLimitReturn' + )) + }) + + 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), [ + 'gasPriceNoDenom', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', '0', 9, true, + ].map(String)) + assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ + 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 21000, '0', + ].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('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, + 8, + false + )) + }) + + it('should render the input-wrapper root node', () => { + assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) + }) + + it('should render an input, but not a GWEI symbol', () => { + assert.equal(gasInput.children().length, 1) + assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) + }) + + it('should show GWEI if the showGWEI prop is truthy', () => { + const gasInputWithGWEI = shallow(wrapper.instance().gasInput( + 321, + value => value + 7, + 0, + 8, + true + )) + assert.equal(gasInputWithGWEI.children().length, 2) + assert(gasInputWithGWEI.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) + assert(gasInputWithGWEI.children().at(1).hasClass('advanced-tab__gas-edit-row__gwei-symbol')) + }) + + 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) + assert.equal(inputProps.precision, 8) + }) + + 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) + }) + }) + +}) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js new file mode 100644 index 000000000..d8490272f --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js @@ -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() + }) + + 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') + }) + }) + +}) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 32f644765..8bf72647a 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -1,6 +1,6 @@ import React from 'react' import assert from 'assert' -import { shallow } from 'enzyme' +import shallow from '../../../../../lib/shallow-with-context' import sinon from 'sinon' import GasModalPageContainer from '../gas-modal-page-container.component.js' @@ -17,6 +17,10 @@ describe('GasModalPageContainer Component', function () { beforeEach(() => { wrapper = shallow( 'mockupdateCustomGasPrice'} + updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} + customGasPrice={21} + customGasLimit={54321} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) @@ -147,10 +151,16 @@ describe('GasModalPageContainer Component', function () { }) describe('renderAdvancedTabContent', () => { - it('should render', () => { + it('should render with the correct props', () => { const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent() - const renderedAdvancedTabContent = shallow(renderAdvancedTabContentResult) - assert.equal(renderedAdvancedTabContent.props().className, 'gas-modal-content__advanced-tab') + const advancedTabContentProps = renderAdvancedTabContentResult.props + assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockupdateCustomGasPrice') + assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockupdateCustomGasLimit') + assert.equal(advancedTabContentProps.customGasPrice, 21) + assert.equal(advancedTabContentProps.customGasLimit, 54321) + assert.equal(advancedTabContentProps.millisecondsRemaining, 91000) + assert.equal(advancedTabContentProps.totalFee, '$0.30') + assert(shallow(renderAdvancedTabContentResult).hasClass('advanced-tab')) }) }) }) diff --git a/ui/lib/shallow-with-context.js b/ui/lib/shallow-with-context.js new file mode 100644 index 000000000..cf83dd76e --- /dev/null +++ b/ui/lib/shallow-with-context.js @@ -0,0 +1,7 @@ +import { shallow } from 'enzyme' + +export default function (jsxComponent) { + return shallow(jsxComponent, { + context: { t: (str1, str2) => str2 ? str1 + str2 : str1 }, + }) +} From 57cd721800aff67cd18e9e530d92eb94ff75d473 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 8 Aug 2018 11:40:17 -0230 Subject: [PATCH 08/46] Improve styling of advanced-tab-content gasInput row --- .../advanced-tab-content.component.js | 2 +- .../advanced-tab-content/index.scss | 10 ++++++++-- .../tests/advanced-tab-content-component.test.js | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 69cd06cde..cd5ca7d35 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -39,7 +39,7 @@ export default class AdvancedTabContent extends Component { } infoButton (onClick) { - return + return } renderDataSummary (totalFee, millisecondsRemaining) { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index 0c95afc48..28e2fde9b 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -67,9 +67,14 @@ color: $mid-gray; font-size: 16px; - .info-circle { + .fa-info-circle { color: $silver; margin-left: 10px; + cursor: pointer; + } + + .fa-info-circle:hover { + color: $mid-gray; } } @@ -85,7 +90,8 @@ font-size: 16px; height: 37px; width: 163px; - padding: 8px 10px 10px 10px; + padding-left: 8px; + padding-top: 2px; } input[type="number"]::-webkit-inner-spin-button { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 3efe1d2a9..5cdcf687e 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -101,7 +101,7 @@ describe('AdvancedTabContent Component', function () { const gasRowLabelChildren = gasEditRow.children().at(0).children() assert.equal(gasRowLabelChildren.length, 2) assert(gasRowLabelChildren.at(0), 'mockLabelKey') - assert(gasRowLabelChildren.at(1).hasClass('info-circle')) + assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle')) }) it('should call this.gasInput with the correct args', () => { @@ -155,7 +155,7 @@ describe('AdvancedTabContent Component', function () { }) it('should render the i element', () => { - assert(infoButton.hasClass('info-circle')) + assert(infoButton.hasClass('fa-info-circle')) }) it('should pass the onClick argument to the i tag onClick prop', () => { From 112d18e316df312a648b8c8ac17c201314fc9ed6 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 9 Aug 2018 13:44:03 -0230 Subject: [PATCH 09/46] Adds basic tab content to gas customizer, with styled button group (static, for now). --- ui/app/components/button/button.component.js | 6 +- .../basic-tab-content.component.js | 22 ++ .../basic-tab-content/index.js | 1 + .../basic-tab-content/index.scss | 11 + .../tests/basic-tab-content-component.test.js | 71 ++++++ .../gas-modal-page-container.component.js | 10 +- .../gas-modal-page-container.container.js | 29 +++ .../gas-modal-page-container/index.scss | 7 +- ...gas-modal-page-container-component.test.js | 47 +++- ...gas-modal-page-container-container.test.js | 39 ++- .../gas-price-button-group.component.js | 85 +++++++ .../gas-price-button-group/index.js | 1 + .../gas-price-button-group/index.scss | 60 +++++ .../gas-price-button-group-component.test.js | 238 ++++++++++++++++++ ui/app/components/index.scss | 2 + ui/app/css/itcss/settings/variables.scss | 1 + 16 files changed, 615 insertions(+), 15 deletions(-) create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss create mode 100644 ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js create mode 100644 ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js create mode 100644 ui/app/components/gas-customization/gas-price-button-group/index.js create mode 100644 ui/app/components/gas-customization/gas-price-button-group/index.scss create mode 100644 ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js index 4a06333e7..5c617585d 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/button/button.component.js @@ -22,7 +22,11 @@ export default class Button extends Component { type: PropTypes.string, large: PropTypes.bool, className: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.array, + PropTypes.element, + ]), } render () { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js new file mode 100644 index 000000000..751042871 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -0,0 +1,22 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import GasPriceButtonGroup from '../../gas-price-button-group' + +export default class BasicTabContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + gasPriceButtonGroupProps: PropTypes.object, + } + + render () { + return ( +
+
Suggest gas fee increases
+ +
+ ) + } +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js new file mode 100644 index 000000000..078d50fce --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.js @@ -0,0 +1 @@ +export { default } from './basic-tab-content.component' diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss new file mode 100644 index 000000000..ba665716c --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss @@ -0,0 +1,11 @@ +.basic-tab-content { + display: flex; + flex-direction: column; + align-items: center; + + &__title { + margin-top: 19px; + font-size: 20px; + color: $scorpion; + } +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js new file mode 100644 index 000000000..0c9c6ac63 --- /dev/null +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js @@ -0,0 +1,71 @@ +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/' + +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() + }) + + 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(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)) + }) + }) +}) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index e47ebaf65..9a0070b2a 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -3,6 +3,7 @@ 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 = { @@ -15,13 +16,14 @@ export default class GasModalPageContainer extends Component { updateCustomGasLimit: PropTypes.func, customGasPrice: PropTypes.number, customGasLimit: PropTypes.number, + gasPriceButtonGroupProps: PropTypes.object, } state = {} renderBasicTabContent () { return ( -
+ ) } @@ -63,7 +65,7 @@ export default class GasModalPageContainer extends Component { renderTabContent (mainTabContent) { return (
- { mainTabContent() } + { mainTabContent } { this.renderInfoRow('gas-modal-content__info-row--fade', 'originalTotal', '$20.02 USD', '0.06685 ETH') } { this.renderInfoRow('gas-modal-content__info-row', 'newTotal', '$20.02 USD', '0.06685 ETH') }
@@ -74,10 +76,10 @@ export default class GasModalPageContainer extends Component { return ( - { this.renderTabContent(this.renderBasicTabContent) } + { this.renderTabContent(this.renderBasicTabContent()) } - { this.renderTabContent(this.renderAdvancedTabContent) } + { this.renderTabContent(this.renderAdvancedTabContent()) } ) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index f7ac91a38..2354d578c 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -10,10 +10,39 @@ import { getCustomGasLimit, } from '../../../selectors/custom-gas' +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, +} + const mapStateToProps = state => { return { customGasPrice: getCustomGasPrice(state), customGasLimit: getCustomGasLimit(state), + gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index ff512537e..427b58888 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -1,4 +1,5 @@ @import './advanced-tab-content/index'; +@import './basic-tab-content/index'; .gas-modal-page-container { .page-container { @@ -12,7 +13,7 @@ } - .info-row, .info-row--fade { + &__info-row, &__info-row--fade { width: 100%; background: $polar; padding: 15px 21px; @@ -51,5 +52,7 @@ &__info-row--fade { background: white; color: $dusty-gray; + border-top: 1px solid $mischka; + margin-top: 22px; } -} \ No newline at end of file +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 8bf72647a..86286b615 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -5,12 +5,43 @@ import sinon from 'sinon' import GasModalPageContainer from '../gas-modal-page-container.component.js' import PageContainer from '../../../page-container' +import BasicTabContent from '../basic-tab-content' +import AdvancedTabContent from '../advanced-tab-content' + import { Tab } from '../../../tabs' const propsMethodSpies = { hideModal: 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, +} + describe('GasModalPageContainer Component', function () { let wrapper @@ -21,6 +52,7 @@ describe('GasModalPageContainer Component', function () { updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} customGasPrice={21} customGasLimit={54321} + gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) @@ -90,8 +122,8 @@ describe('GasModalPageContainer Component', function () { assert.equal(GP.renderTabContent.callCount, 2) - assert.deepEqual(GP.renderTabContent.firstCall.args, [wrapper.instance().renderBasicTabContent]) - assert.deepEqual(GP.renderTabContent.secondCall.args, [wrapper.instance().renderAdvancedTabContent]) + assert.equal(GP.renderTabContent.firstCall.args.type, BasicTabContent.type) + assert.equal(GP.renderTabContent.secondCall.args.type, AdvancedTabContent.type) GP.renderTabContent.restore() }) @@ -104,8 +136,8 @@ describe('GasModalPageContainer Component', function () { assert.equal(renderedTabContent.props().className, 'gas-modal-content') }) - it('should render the element returned by the passed func as its first child', () => { - const renderTabContentResult = wrapper.instance().renderTabContent(() => Mock content) + it('should render the passed element as its first child', () => { + const renderTabContentResult = wrapper.instance().renderTabContent(Mock content) const renderedTabContent = shallow(renderTabContentResult) assert(renderedTabContent.childAt(0).equals(Mock content)) }) @@ -145,8 +177,11 @@ describe('GasModalPageContainer Component', function () { describe('renderBasicTabContent', () => { it('should render', () => { const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent() - const renderedBasicTabContent = shallow(renderBasicTabContentResult) - assert.equal(renderedBasicTabContent.props().className, 'gas-modal-content__basic-tab') + + assert.deepEqual( + renderBasicTabContentResult.props.gasPriceButtonGroupProps, + mockGasPriceButtonGroupProps + ) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 5b133fbe2..119ae640b 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -2,26 +2,45 @@ import assert from 'assert' import proxyquire from 'proxyquire' import sinon from 'sinon' -// let mapStateToProps +let mapStateToProps let mapDispatchToProps const actionSpies = { hideModal: sinon.spy(), } +const customGasActionSpies = { + setCustomGasPrice: sinon.spy(), + setCustomGasLimit: sinon.spy(), +} + proxyquire('../gas-modal-page-container.container.js', { 'react-redux': { connect: (ms, md) => { - // mapStateToProps = ms + mapStateToProps = ms mapDispatchToProps = md return () => ({}) }, }, + '../../../selectors/custom-gas': { + getCustomGasPrice: (s) => `mockGasPrice:${s}`, + getCustomGasLimit: (s) => `mockGasLimit:${s}`, + }, '../../../actions': actionSpies, + '../../../ducks/custom-gas': customGasActionSpies, }) describe('gas-modal-page-container container', () => { + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.equal(mapStateToProps('mockState').customGasPrice, 'mockGasPrice:mockState') + assert.equal(mapStateToProps('mockState').customGasLimit, 'mockGasLimit:mockState') + }) + + }) + describe('mapDispatchToProps()', () => { let dispatchSpy let mapDispatchToPropsObject @@ -39,6 +58,22 @@ describe('gas-modal-page-container container', () => { }) }) + describe('updateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action', () => { + mapDispatchToPropsObject.updateCustomGasPrice() + assert(dispatchSpy.calledOnce) + assert(customGasActionSpies.setCustomGasPrice.calledOnce) + }) + }) + + describe('updateCustomGasLimit()', () => { + it('should dispatch a setCustomGasLimit action', () => { + mapDispatchToPropsObject.updateCustomGasLimit() + assert(dispatchSpy.calledOnce) + assert(customGasActionSpies.setCustomGasLimit.calledOnce) + }) + }) + }) }) diff --git a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js new file mode 100644 index 000000000..3873f54bc --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -0,0 +1,85 @@ +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, + noButtonActiveByDefault: PropTypes.bool, + showCheck: PropTypes.bool, + } + + renderButtonContent ({ + label, + feeInPrimaryCurrency, + feeInSecondaryCurrency, + timeEstimate, + }, { + className, + showCheck, + }) { + return (
+ { label &&
{ label }
} + { feeInPrimaryCurrency &&
{ feeInPrimaryCurrency }
} + { feeInSecondaryCurrency &&
{ feeInSecondaryCurrency }
} + { timeEstimate &&
{ timeEstimate }
} + { showCheck && } +
) + } + + renderButton ({ + priceInHexWei, + ...renderableGasInfo + }, { + buttonDataLoading, + handleGasPriceSelection, + ...buttonContentPropsAndFlags + }, index) { + return ( + + ) + } + + render () { + const { + gasButtonInfo, + defaultActiveButtonIndex = 1, + noButtonActiveByDefault = false, + ...buttonPropsAndFlags + } = this.props + + return ( + + { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } + + ) + } +} diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.js b/ui/app/components/gas-customization/gas-price-button-group/index.js new file mode 100644 index 000000000..775648330 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-button-group/index.js @@ -0,0 +1 @@ +export { default } from './gas-price-button-group.component' diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/gas-customization/gas-price-button-group/index.scss new file mode 100644 index 000000000..e2670edd5 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-button-group/index.scss @@ -0,0 +1,60 @@ +.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; + } + + + .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 + } + } + } +} diff --git a/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js new file mode 100644 index 000000000..e1458188d --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js @@ -0,0 +1,238 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +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({}, { + buttonDataLoading: mockGasPriceButtonGroupProps.buttonDataLoading, + 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() + }) + + 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 called this.renderButton 3 times, with the correct args', () => { + assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3) + renderButtonArgsTest(0, mockButtonPropsAndFlags) + renderButtonArgsTest(1, mockButtonPropsAndFlags) + renderButtonArgsTest(2, mockButtonPropsAndFlags) + }) + }) + + 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, + }, + ] + ) + }) + + it('should not call renderButtonContent if buttonDataLoading is true and should show a loading indicator', () => { + GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() + const renderButtonResult = GasPriceButtonGroup.prototype.renderButton( + Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]), + Object.assign({}, mockButtonPropsAndFlags, {buttonDataLoading: true}) + ) + wrappedRenderButtonResult = shallow(renderButtonResult) + assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 0) + assert.equal(wrappedRenderButtonResult.childAt(0).text(), 'Loading...') + }) + }) + + describe('renderButtonContent', () => { + it('should render a label if passed a label', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ + label: 'mockLabel', + }, { + className: 'someClass', + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) + assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabel') + }) + + 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 = GasPriceButtonGroup.prototype.renderButtonContent({ + label: '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) + }) + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 156b1b9f6..78c1216f7 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -71,3 +71,5 @@ @import './gas-customization/gas-modal-page-container/index'; @import './gas-customization/index'; + +@import './gas-customization/gas-price-button-group/index'; diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 1c95f06a0..42a8655df 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -58,6 +58,7 @@ $linen: #fdf4f4; $oslo-gray: #8C8E94; $polar: #fafcfe; $blizzard-blue: #bfdef3; +$mischka: #dddee9; /* Z-Indicies From 0a7dfcd55d02a7204d8f0773ff9d91f325aabea8 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 16 Aug 2018 09:58:27 -0230 Subject: [PATCH 10/46] Connect the gas-button-group component to redux and a live api. --- .../button-group/button-group.component.js | 5 +- .../basic-tab-content.component.js | 8 +- .../gas-modal-page-container.component.js | 4 +- .../gas-modal-page-container.container.js | 39 +--- ...gas-modal-page-container-container.test.js | 6 +- .../gas-customization/gas.selectors.js | 14 ++ ui/app/components/send/send.component.js | 4 + ui/app/components/send/send.container.js | 4 + .../send/tests/send-component.test.js | 12 ++ ui/app/ducks/custom-gas.js | 67 ------- ui/app/ducks/gas.duck.js | 189 ++++++++++++++++++ ui/app/helpers/confirm-transaction/util.js | 2 +- ui/app/reducers.js | 4 +- ui/app/selectors/custom-gas.js | 186 ++++++++++++++++- ui/app/selectors/tests/custom-gas.test.js | 140 +++++++++++++ 15 files changed, 573 insertions(+), 111 deletions(-) create mode 100644 ui/app/components/gas-customization/gas.selectors.js delete mode 100644 ui/app/ducks/custom-gas.js create mode 100644 ui/app/ducks/gas.duck.js create mode 100644 ui/app/selectors/tests/custom-gas.test.js diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js index f99f710ce..905bbe9d2 100644 --- a/ui/app/components/button-group/button-group.component.js +++ b/ui/app/components/button-group/button-group.component.js @@ -5,6 +5,7 @@ import classnames from 'classnames' export default class ButtonGroup extends PureComponent { static propTypes = { defaultActiveButtonIndex: PropTypes.number, + noButtonActiveByDefault: PropTypes.bool, disabled: PropTypes.bool, children: PropTypes.array, className: PropTypes.string, @@ -16,7 +17,9 @@ export default class ButtonGroup extends PureComponent { } state = { - activeButtonIndex: this.props.defaultActiveButtonIndex || 0, + activeButtonIndex: this.props.noButtonActiveByDefault + ? null + : this.props.defaultActiveButtonIndex || 0, } handleButtonClick (activeButtonIndex) { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 751042871..99ef28b5e 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -15,7 +15,13 @@ export default class BasicTabContent extends Component { return (
Suggest gas fee increases
- + console.log('New Price:', newPrice)} + />
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 9a0070b2a..399520baf 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -23,7 +23,9 @@ export default class GasModalPageContainer extends Component { renderBasicTabContent () { return ( - + ) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 2354d578c..ebdd035ea 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -4,45 +4,23 @@ import { hideModal } from '../../../actions' import { setCustomGasPrice, setCustomGasLimit, -} from '../../../ducks/custom-gas' +} from '../../../ducks/gas.duck' import { getCustomGasPrice, getCustomGasLimit, + getRenderableBasicEstimateData, + getBasicGasEstimateLoadingStatus, } from '../../../selectors/custom-gas' -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, -} - const mapStateToProps = state => { + const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) return { customGasPrice: getCustomGasPrice(state), customGasLimit: getCustomGasLimit(state), - gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, + gasPriceButtonGroupProps: { + buttonDataLoading, + gasButtonInfo: getRenderableBasicEstimateData(state), + }, } } @@ -51,6 +29,7 @@ const mapDispatchToProps = dispatch => { hideModal: () => dispatch(hideModal()), updateCustomGasPrice: (newPrice) => dispatch(setCustomGasPrice(newPrice)), updateCustomGasLimit: (newLimit) => dispatch(setCustomGasLimit(newLimit)), + handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice), } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 119ae640b..4067265b5 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -25,9 +25,11 @@ proxyquire('../gas-modal-page-container.container.js', { '../../../selectors/custom-gas': { getCustomGasPrice: (s) => `mockGasPrice:${s}`, getCustomGasLimit: (s) => `mockGasLimit:${s}`, + getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, + getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${s}`, }, '../../../actions': actionSpies, - '../../../ducks/custom-gas': customGasActionSpies, + '../../../ducks/gas.duck': customGasActionSpies, }) describe('gas-modal-page-container container', () => { @@ -37,6 +39,8 @@ describe('gas-modal-page-container container', () => { it('should map the correct properties to props', () => { assert.equal(mapStateToProps('mockState').customGasPrice, 'mockGasPrice:mockState') assert.equal(mapStateToProps('mockState').customGasLimit, 'mockGasLimit:mockState') + assert.equal(mapStateToProps('mockState').gasPriceButtonGroupProps.buttonDataLoading, 'mockBasicGasEstimateLoadingStatus:mockState') + assert.equal(mapStateToProps('mockState').gasPriceButtonGroupProps.gasButtonInfo, 'mockRenderableBasicEstimateData:mockState') }) }) diff --git a/ui/app/components/gas-customization/gas.selectors.js b/ui/app/components/gas-customization/gas.selectors.js new file mode 100644 index 000000000..89374b5f1 --- /dev/null +++ b/ui/app/components/gas-customization/gas.selectors.js @@ -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 +} diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index a27401f30..a639c24b0 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -162,6 +162,10 @@ export default class SendTransactionScreen extends PersistentForm { } } + componentDidMount () { + this.props.fetchGasEstimates() + } + componentWillMount () { const { from: { address }, diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index 87056499f..a00fb3545 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -36,6 +36,9 @@ import { resetSendState, updateSendErrors, } from '../../ducks/send.duck' +import { + fetchGasEstimates, +} from '../../ducks/gas.duck' import { calcGasTotal, } from './send.utils.js' @@ -104,5 +107,6 @@ function mapDispatchToProps (dispatch) { scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), + fetchGasEstimates: () => dispatch(fetchGasEstimates()), } } diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js index bd136a0b8..0f3902c07 100644 --- a/ui/app/components/send/tests/send-component.test.js +++ b/ui/app/components/send/tests/send-component.test.js @@ -13,6 +13,7 @@ const propsMethodSpies = { updateSendErrors: sinon.spy(), updateSendTokenBalance: sinon.spy(), resetSendState: sinon.spy(), + fetchGasEstimates: sinon.spy(), } const utilsMethodStubs = { getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), @@ -37,6 +38,7 @@ describe('Send Component', function () { blockGasLimit={'mockBlockGasLimit'} conversionRate={10} editingTransactionId={'mockEditingTransactionId'} + fetchGasEstimates={propsMethodSpies.fetchGasEstimates} from={ { address: 'mockAddress', balance: 'mockBalance' } } gasLimit={'mockGasLimit'} gasPrice={'mockGasPrice'} @@ -63,6 +65,7 @@ describe('Send Component', function () { utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory() utilsMethodStubs.getAmountErrorObject.resetHistory() utilsMethodStubs.getGasFeeErrorObject.resetHistory() + propsMethodSpies.fetchGasEstimates.resetHistory() propsMethodSpies.updateAndSetGasTotal.resetHistory() propsMethodSpies.updateSendErrors.resetHistory() propsMethodSpies.updateSendTokenBalance.resetHistory() @@ -82,6 +85,15 @@ describe('Send Component', function () { }) }) + describe('componentDidMount', () => { + it('should call props.fetchGasEstimates', () => { + propsMethodSpies.fetchGasEstimates.resetHistory() + assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0) + wrapper.instance().componentDidMount() + assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1) + }) + }) + describe('componentWillUnmount', () => { it('should call this.props.resetSendState', () => { propsMethodSpies.resetSendState.resetHistory() diff --git a/ui/app/ducks/custom-gas.js b/ui/app/ducks/custom-gas.js deleted file mode 100644 index f1f483e93..000000000 --- a/ui/app/ducks/custom-gas.js +++ /dev/null @@ -1,67 +0,0 @@ -import extend from 'xtend' - -// Actions -const SET_CUSTOM_GAS_PRICE = 'metamask/custom-gas/SET_CUSTOM_GAS_PRICE' -const SET_CUSTOM_GAS_LIMIT = 'metamask/custom-gas/SET_CUSTOM_GAS_LIMIT' -const SET_CUSTOM_GAS_ERRORS = 'metamask/custom-gas/SET_CUSTOM_GAS_ERRORS' -const RESET_CUSTOM_GAS_STATE = 'metamask/custom-gas/RESET_CUSTOM_GAS_STATE' - -// TODO: determine if this approach to initState is consistent with conventional ducks pattern -const initState = { - price: 0, - limit: 21000, - errors: {}, -} - -// Reducer -export default function reducer ({ customGas: customGasState = initState }, action = {}) { - const newState = extend({}, customGasState) - - switch (action.type) { - case SET_CUSTOM_GAS_PRICE: - return extend(newState, { - price: action.value, - }) - case SET_CUSTOM_GAS_LIMIT: - return extend(newState, { - limit: action.value, - }) - case SET_CUSTOM_GAS_ERRORS: - return extend(newState, { - errors: { - ...newState.errors, - ...action.value, - }, - }) - case RESET_CUSTOM_GAS_STATE: - return extend({}, initState) - default: - return newState - } -} - -// Action Creators -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 setCustomGasErrors (newErrors) { - return { - type: SET_CUSTOM_GAS_ERRORS, - value: newErrors, - } -} - -export function resetCustomGasState () { - return { type: RESET_CUSTOM_GAS_STATE } -} diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js new file mode 100644 index 000000000..8b2fbcfdb --- /dev/null +++ b/ui/app/ducks/gas.duck.js @@ -0,0 +1,189 @@ +import { clone } from 'ramda' + +// 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 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' + +// TODO: determine if this approach to initState is consistent with conventional ducks pattern +const initState = { + customData: { + price: 0, + limit: 21000, + }, + 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: {}, +} + +// 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 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_CUSTOM_GAS_ERRORS: + return { + ...newState, + errors: { + ...newState.errors, + ...action.value, + }, + } + 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 fetchGasEstimates () { + return (dispatch) => { + dispatch(basicGasEstimatesLoadingStarted()) + + return 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, + avgWait, + block_time: blockTime, + blockNum, + fast, + fastest, + fastestWait, + fastWait, + safeLow, + safeLowWait, + speed, + }) => { + dispatch(setBasicGasEstimateData({ + average, + avgWait, + blockTime, + blockNum, + fast, + fastest, + fastestWait, + fastWait, + safeLow, + safeLowWait, + speed, + })) + dispatch(basicGasEstimatesLoadingFinished()) + }) + } +} + +export function setBasicGasEstimateData (basicGasEstimateData) { + return { + type: SET_BASIC_GAS_ESTIMATE_DATA, + value: basicGasEstimateData, + } +} + +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 resetCustomGasState () { + return { type: RESET_CUSTOM_GAS_STATE } +} diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js index eb334a4b8..0451824e8 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/confirm-transaction/util.js @@ -95,7 +95,7 @@ export function formatCurrency (value, currencyCode) { const upperCaseCurrencyCode = currencyCode.toUpperCase() return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode }) + ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode, style: 'currency' }) : value } diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 7234c3c02..786af853d 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -10,7 +10,7 @@ const reduceApp = require('./reducers/app') const reduceLocale = require('./reducers/locale') const reduceSend = require('./ducks/send.duck').default import reduceConfirmTransaction from './ducks/confirm-transaction.duck' -import reduceCustomGas from './ducks/custom-gas' +import reduceGas from './ducks/gas.duck' window.METAMASK_CACHED_LOG_STATE = null @@ -50,7 +50,7 @@ function rootReducer (state, action) { state.confirmTransaction = reduceConfirmTransaction(state, action) - state.customGas = reduceCustomGas(state, action) + state.gas = reduceGas(state, action) window.METAMASK_CACHED_LOG_STATE = state return state diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 97280190f..ff2fd0e05 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -1,19 +1,191 @@ +import { pipe, partialRight } from 'ramda' +import { + getConversionRate, + getGasLimit, +} from '../components/send/send.selectors' +import { + conversionUtil, + multiplyCurrencies, +} from '../conversion-util' +import { + getCurrentCurrency, +} from '../selectors' +import { + formatCurrency, +} from '../helpers/confirm-transaction/util' +import { + calcGasTotal, +} from '../components/send/send.utils' +import { addHexPrefix } from 'ethereumjs-util' + const selectors = { - getCustomGasPrice, - getCustomGasLimit, getCustomGasErrors, + getCustomGasLimit, + getCustomGasPrice, + getCustomGasTotal, + getRenderableBasicEstimateData, + getBasicGasEstimateLoadingStatus, } module.exports = selectors -function getCustomGasPrice (state) { - return state.customGas.price +function getCustomGasErrors (state) { + return state.gas.errors } function getCustomGasLimit (state) { - return state.customGas.limit + return state.gas.customData.limit } -function getCustomGasErrors (state) { - return state.customGas.errors +function getCustomGasPrice (state) { + return state.gas.customData.price +} + +function getCustomGasTotal (state) { + return state.gas.customData.total +} + +function getBasicGasEstimateLoadingStatus (state) { + return state.gas.basicEstimateIsLoading +} + + +function apiEstimateModifiedToGWEI (estimate) { + return multiplyCurrencies(estimate, 0.10, { + toNumericBase: 'hex', + multiplicandBase: 10, + multiplierBase: 10, + numberOfDecimals: 9, + }) +} + +function basicPriceEstimateToETHTotal (estimate, gasLimit) { + return conversionUtil(calcGasTotal(gasLimit, estimate), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'GWEI', + numberOfDecimals: 9, + }) +} + +function ethTotalToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) { + return conversionUtil(ethTotal, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: convertedCurrency, + numberOfDecimals: 2, + conversionRate, + }) +} + +function formatETHFee (ethFee) { + return ethFee + ' ETH' +} + +function getRenderableEthFee (estimate, gasLimit) { + return pipe( + apiEstimateModifiedToGWEI, + partialRight(basicPriceEstimateToETHTotal, [gasLimit]), + formatETHFee + )(estimate, gasLimit) +} + +function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) { + return pipe( + apiEstimateModifiedToGWEI, + partialRight(basicPriceEstimateToETHTotal, [gasLimit]), + partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), + partialRight(formatCurrency, [convertedCurrency]) + )(estimate, gasLimit, convertedCurrency, conversionRate) +} + +function getTimeEstimateInSeconds (blockWaitEstimate, currentBlockTime) { + return multiplyCurrencies(blockWaitEstimate, currentBlockTime, { + toNumericBase: 'dec', + multiplicandBase: 10, + multiplierBase: 10, + numberOfDecimals: 1, + }) +} + +function formatTimeEstimate (totalSeconds) { + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + const formattedMin = `${minutes ? minutes + ' min' : ''}` + const formattedSec = `${seconds ? seconds + ' sec' : ''}` + const formattedCombined = formattedMin && formattedSec + ? `~${formattedMin} ${formattedSec}` + : '~' + [formattedMin, formattedSec].find(t => t) + + return formattedCombined +} + +function getRenderableTimeEstimate (blockWaitEstimate, currentBlockTime) { + return pipe( + getTimeEstimateInSeconds, + formatTimeEstimate + )(blockWaitEstimate, currentBlockTime) +} + +function priceEstimateToWei (priceEstimate) { + return conversionUtil(priceEstimate, { + fromNumericBase: 'hex', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + numberOfDecimals: 9, + }) +} + +function getGasPriceInHexWei (price) { + return pipe( + apiEstimateModifiedToGWEI, + priceEstimateToWei, + addHexPrefix + )(price) +} + +function getRenderableBasicEstimateData (state) { + if (getBasicGasEstimateLoadingStatus(state)) { + return [] + } + + const gasLimit = getGasLimit(state) + const conversionRate = getConversionRate(state) + const currentCurrency = getCurrentCurrency(state) + const { + gas: { + basicEstimates: { + safeLow, + average, + fast, + blockTime, + safeLowWait, + avgWait, + fastWait, + }, + }, + } = state + + return [ + { + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(fast, gasLimit), + timeEstimate: getRenderableTimeEstimate(fastWait, blockTime), + priceInHexWei: getGasPriceInHexWei(fast), + }, + { + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(average, gasLimit), + timeEstimate: getRenderableTimeEstimate(avgWait, blockTime), + priceInHexWei: getGasPriceInHexWei(average), + }, + { + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(safeLow, gasLimit), + timeEstimate: getRenderableTimeEstimate(safeLowWait, blockTime), + priceInHexWei: getGasPriceInHexWei(safeLow), + }, + ] } diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js new file mode 100644 index 000000000..d53c1ec98 --- /dev/null +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -0,0 +1,140 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +const { + getCustomGasErrors, + getCustomGasLimit, + getCustomGasPrice, + getCustomGasTotal, + getRenderableBasicEstimateData, +} = proxyquire('../custom-gas', {}) + +describe('custom-gas selectors', () => { + + describe('getCustomGasPrice()', () => { + it('should return gas.customData.price', () => { + const mockState = { gas: { customData: { price: 'mockPrice' } } } + assert.equal(getCustomGasPrice(mockState), 'mockPrice') + }) + }) + + describe('getCustomGasLimit()', () => { + it('should return gas.customData.limit', () => { + const mockState = { gas: { customData: { limit: 'mockLimit' } } } + assert.equal(getCustomGasLimit(mockState), 'mockLimit') + }) + }) + + describe('getCustomGasTotal()', () => { + it('should return gas.customData.total', () => { + const mockState = { gas: { customData: { total: 'mockTotal' } } } + assert.equal(getCustomGasTotal(mockState), 'mockTotal') + }) + }) + + describe('getCustomGasErrors()', () => { + it('should return gas.errors', () => { + const mockState = { gas: { errors: 'mockErrors' } } + assert.equal(getCustomGasErrors(mockState), 'mockErrors') + }) + }) + + describe('getRenderableBasicEstimateData()', () => { + const tests = [ + { + expectedResult: [ + { + feeInPrimaryCurrency: '$0.05', + feeInSecondaryCurrency: '0.00021 ETH', + timeEstimate: '~7 sec', + priceInHexWei: '0x2540be400', + }, + { + feeInPrimaryCurrency: '$0.03', + feeInSecondaryCurrency: '0.000105 ETH', + timeEstimate: '~46 sec', + priceInHexWei: '0x12a05f200', + }, + { + feeInPrimaryCurrency: '$0.01', + feeInSecondaryCurrency: '0.0000525 ETH', + timeEstimate: '~1 min 33 sec', + priceInHexWei: '0x9502f900', + }, + ], + mockState: { + metamask: { + conversionRate: 255.71, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 25, + safeLowWait: 6.6, + average: 50, + avgWait: 3.3, + fast: 100, + fastWait: 0.5, + }, + }, + }, + }, + { + expectedResult: [ + { + feeInPrimaryCurrency: '$1.07', + feeInSecondaryCurrency: '0.00042 ETH', + timeEstimate: '~14 sec', + priceInHexWei: '0x4a817c800', + }, + { + feeInPrimaryCurrency: '$0.54', + feeInSecondaryCurrency: '0.00021 ETH', + timeEstimate: '~1 min 33 sec', + priceInHexWei: '0x2540be400', + }, + { + feeInPrimaryCurrency: '$0.27', + feeInSecondaryCurrency: '0.000105 ETH', + timeEstimate: '~3 min 7 sec', + priceInHexWei: '0x12a05f200', + }, + ], + mockState: { + metamask: { + conversionRate: 2557.1, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 50, + safeLowWait: 13.2, + average: 100, + avgWait: 6.6, + fast: 200, + fastWait: 1.0, + }, + }, + }, + }, + ] + it('should return renderable data about basic estimates', () => { + tests.forEach(test => { + assert.deepEqual( + getRenderableBasicEstimateData(test.mockState), + test.expectedResult + ) + }) + }) + + }) + +}) From d07d40cf7cf013bd9b9968fa6bc37bcfd1367cf9 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Sun, 9 Sep 2018 13:48:34 -0230 Subject: [PATCH 11/46] Improvements to propdefaults in button-group.component and basic-tab-content.component --- ui/app/components/button-group/button-group.component.js | 3 ++- .../basic-tab-content/basic-tab-content.component.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js index 905bbe9d2..440b3c4f7 100644 --- a/ui/app/components/button-group/button-group.component.js +++ b/ui/app/components/button-group/button-group.component.js @@ -14,12 +14,13 @@ export default class ButtonGroup extends PureComponent { static defaultProps = { className: 'button-group', + defaultActiveButtonIndex: 0, } state = { activeButtonIndex: this.props.noButtonActiveByDefault ? null - : this.props.defaultActiveButtonIndex || 0, + : this.props.defaultActiveButtonIndex, } handleButtonClick (activeButtonIndex) { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 99ef28b5e..164da4a32 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -16,11 +16,11 @@ export default class BasicTabContent extends Component {
Suggest gas fee increases
console.log('New Price:', newPrice)} + {...this.props.gasPriceButtonGroupProps} />
) From 58feb24fa72bac5c79daa96956810dd2233adf02 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Sun, 9 Sep 2018 15:17:49 -0230 Subject: [PATCH 12/46] Styling fixes for gas customization advanced tab content. --- .../advanced-tab-content/advanced-tab-content.component.js | 2 +- .../gas-modal-page-container/advanced-tab-content/index.scss | 1 - .../gas-modal-page-container/basic-tab-content/index.scss | 1 + .../gas-customization/gas-modal-page-container/index.scss | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index cd5ca7d35..38fa117f9 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -39,7 +39,7 @@ export default class AdvancedTabContent extends Component { } infoButton (onClick) { - return + return } renderDataSummary (totalFee, millisecondsRemaining) { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index 28e2fde9b..aced75449 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -3,7 +3,6 @@ .advanced-tab { display: flex; flex-flow: column; - border-bottom: 1px solid $alto; height: 430px; &__transaction-data-summary, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss index ba665716c..d5eb5d414 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss @@ -2,6 +2,7 @@ display: flex; flex-direction: column; align-items: center; + margin-bottom: 22px; &__title { margin-top: 19px; diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 427b58888..f02045fe3 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -53,6 +53,5 @@ background: white; color: $dusty-gray; border-top: 1px solid $mischka; - margin-top: 22px; } } From 6ada9b4a3c02014f8d00b1f45a149afbf47f700d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Sun, 9 Sep 2018 16:55:54 -0230 Subject: [PATCH 13/46] Adds gas-duck.test.js tests. --- ui/app/ducks/tests/gas-duck.test.js | 300 ++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 ui/app/ducks/tests/gas-duck.test.js diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js new file mode 100644 index 000000000..7fd74f815 --- /dev/null +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -0,0 +1,300 @@ +import assert from 'assert' +import sinon from 'sinon' + +import GasReducer, { + basicGasEstimatesLoadingStarted, + basicGasEstimatesLoadingFinished, + setBasicGasEstimateData, + setCustomGasPrice, + setCustomGasLimit, + setCustomGasTotal, + setCustomGasErrors, + resetCustomGasState, + fetchGasEstimates, +} from '../gas.duck.js' + +describe('Gas Duck', () => { + let tempFetch + const fetchStub = sinon.stub().returns(new Promise(resolve => resolve({ + json: () => new Promise(resolve => resolve({ + average: 'mockAverage', + avgWait: 'mockAvgWait', + block_time: 'mockBlock_time', + blockNum: 'mockBlockNum', + fast: 'mockFast', + fastest: 'mockFastest', + fastestWait: 'mockFastestWait', + fastWait: 'mockFastWait', + safeLow: 'mockSafeLow', + safeLowWait: 'mockSafeLowWait', + speed: 'mockSpeed', + })), + }))) + + beforeEach(() => { + tempFetch = global.fetch + global.fetch = fetchStub + }) + + afterEach(() => { + global.fetch = tempFetch + }) + + const mockState = { + gas: { + mockProp: 123, + }, + } + const initState = { + customData: { + price: 0, + limit: 21000, + }, + 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: {}, + } + 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 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' + + 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 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 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 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('fetchGasEstimates', () => { + const mockDistpatch = sinon.spy() + it('should call fetch with the expected params', async () => { + await fetchGasEstimates()(mockDistpatch) + 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_GAS_ESTIMATE_DATA, + value: { + average: 'mockAverage', + avgWait: 'mockAvgWait', + blockTime: 'mockBlock_time', + blockNum: 'mockBlockNum', + fast: 'mockFast', + fastest: 'mockFastest', + fastestWait: 'mockFastestWait', + fastWait: 'mockFastWait', + safeLow: 'mockSafeLow', + safeLowWait: 'mockSafeLowWait', + speed: 'mockSpeed', + }, + }] + ) + assert.deepEqual( + mockDistpatch.getCall(2).args, + [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }] + ) + }) + }) + + + 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('resetCustomGasState', () => { + it('should create the correct action', () => { + assert.deepEqual( + resetCustomGasState(), + { type: RESET_CUSTOM_GAS_STATE } + ) + }) + }) + +}) From 7de3f22d63748ed5a81e947755db056d4cdef3db Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 13 Sep 2018 06:17:05 -0230 Subject: [PATCH 14/46] Connects remained of the gas customization component to redux. --- ui/app/actions.js | 42 ++---- .../advanced-tab-content.component.js | 8 +- .../advanced-tab-content-component.test.js | 4 +- .../basic-tab-content.component.js | 2 - .../gas-modal-page-container.component.js | 80 +++++++--- .../gas-modal-page-container.container.js | 141 +++++++++++++++++- ...gas-modal-page-container-component.test.js | 97 ++++++------ ...gas-modal-page-container-container.test.js | 122 ++++++++++++--- .../confirm-transaction-base.container.js | 14 +- .../confirm-transaction.component.js | 3 + .../confirm-transaction.container.js | 4 + ui/app/components/send/send.component.js | 9 +- ui/app/components/send/send.container.js | 4 +- ui/app/components/send/send.selectors.js | 10 +- .../send/tests/send-component.test.js | 46 +++--- .../send/tests/send-container.test.js | 10 +- .../send/tests/send-selectors.test.js | 2 +- ui/app/ducks/gas.duck.js | 4 +- ui/app/ducks/tests/gas-duck.test.js | 4 +- ui/app/helpers/conversions.util.js | 39 +++++ ui/app/helpers/formatters.js | 3 + ui/app/selectors/custom-gas.js | 43 +++--- 22 files changed, 497 insertions(+), 194 deletions(-) create mode 100644 ui/app/helpers/formatters.js diff --git a/ui/app/actions.js b/ui/app/actions.js index e3cf57c9e..b747a97fb 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -3,7 +3,6 @@ const pify = require('pify') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const { getTokenAddressFromTokenObject } = require('./util') const { - calcGasTotal, calcTokenBalance, estimateGas, } = require('./components/send/send.utils') @@ -12,6 +11,7 @@ const { fetchLocale } = require('../i18n-helper') const log = require('loglevel') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums') const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') +const gasDuck = require('./ducks/gas.duck') const WebcamUtils = require('../lib/webcam-utils') var actions = { @@ -921,6 +921,7 @@ function setGasTotal (gasTotal) { } function updateGasData ({ + gasPrice, blockGasLimit, recentBlocks, selectedAddress, @@ -931,34 +932,19 @@ function updateGasData ({ }) { return (dispatch) => { dispatch(actions.gasLoadingStarted()) - return new Promise((resolve, reject) => { - background.getGasPrice((err, data) => { - if (err) return reject(err) - return resolve(data) - }) - }) - .then(estimateGasPrice => { - return Promise.all([ - Promise.resolve(estimateGasPrice), - estimateGas({ - estimateGasMethod: background.estimateGas, - blockGasLimit, - selectedAddress, - selectedToken, - to, - value, - estimateGasPrice, - data, - }), - ]) - }) - .then(([gasPrice, gas]) => { - dispatch(actions.setGasPrice(gasPrice)) + return estimateGas({ + estimateGasMethod: background.estimateGas, + blockGasLimit, + selectedAddress, + selectedToken, + to, + value, + estimateGasPrice: gasPrice, + data, + }) + .then(gas => { dispatch(actions.setGasLimit(gas)) - return calcGasTotal(gas, gasPrice) - }) - .then((gasEstimate) => { - dispatch(actions.setGasTotal(gasEstimate)) + dispatch(gasDuck.setCustomGasLimit(gas)) dispatch(updateSendErrors({ gasLoadingError: null })) dispatch(actions.gasLoadingFinished()) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 38fa117f9..56d10cc2b 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -1,9 +1,5 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import { - MIN_GAS_PRICE_DEC, - MIN_GAS_LIMIT_DEC, -} from '../../../send/send.constants' import TimeRemaining from './time-remaining' export default class AdvancedTabContent extends Component { @@ -76,8 +72,8 @@ export default class AdvancedTabContent extends Component { renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit) { return (
- { this.renderGasEditRow('gasPriceNoDenom', customGasPrice, updateCustomGasPrice, MIN_GAS_PRICE_DEC, 9, true) } - { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, MIN_GAS_LIMIT_DEC, 0) } + { this.renderGasEditRow('gasPriceNoDenom', customGasPrice, updateCustomGasPrice, customGasPrice, 9, true) } + { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, customGasLimit, 0) }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 5cdcf687e..0ef286b8a 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -138,10 +138,10 @@ describe('AdvancedTabContent Component', function () { const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args assert.equal(renderGasEditRowSpyArgs.length, 2) assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ - 'gasPriceNoDenom', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', '0', 9, true, + 'gasPriceNoDenom', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', 9, true, ].map(String)) assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ - 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 21000, '0', + 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 'mockGasLimit', 0, ].map(String)) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 164da4a32..0e5f15519 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -17,9 +17,7 @@ export default class BasicTabContent extends Component {
Suggest gas fee increases
console.log('New Price:', newPrice)} {...this.props.gasPriceButtonGroupProps} />
diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 399520baf..89065472d 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -17,34 +17,42 @@ export default class GasModalPageContainer extends Component { customGasPrice: PropTypes.number, customGasLimit: PropTypes.number, gasPriceButtonGroupProps: PropTypes.object, + infoRowProps: PropTypes.shape({ + originalTotalFiat: PropTypes.string, + originalTotalEth: PropTypes.string, + newTotalFiat: PropTypes.string, + newTotalEth: PropTypes.string, + }), + onSubmit: PropTypes.func, + customGasPriceInHex: PropTypes.string, + customGasLimitInHex: PropTypes.string, } state = {} - renderBasicTabContent () { + renderBasicTabContent (gasPriceButtonGroupProps) { return ( ) } - renderAdvancedTabContent = () => { - const { - updateCustomGasPrice, - updateCustomGasLimit, - customGasPrice, - customGasLimit, - } = this.props - + renderAdvancedTabContent ({ + convertThenUpdateCustomGasPrice, + convertThenUpdateCustomGasLimit, + customGasPrice, + customGasLimit, + newTotalFiat, + }) { return ( ) } @@ -64,41 +72,67 @@ export default class GasModalPageContainer extends Component { ) } - renderTabContent (mainTabContent) { + renderInfoRows (originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) { return ( -
- { mainTabContent } - { this.renderInfoRow('gas-modal-content__info-row--fade', 'originalTotal', '$20.02 USD', '0.06685 ETH') } - { this.renderInfoRow('gas-modal-content__info-row', 'newTotal', '$20.02 USD', '0.06685 ETH') } +
+ { this.renderInfoRow('gas-modal-content__info-row--fade', 'originalTotal', originalTotalFiat, originalTotalEth) } + { this.renderInfoRow('gas-modal-content__info-row', 'newTotal', newTotalFiat, newTotalEth) }
) } - renderTabs () { + renderTabs ({ + originalTotalFiat, + originalTotalEth, + newTotalFiat, + newTotalEth, + }, + { + gasPriceButtonGroupProps, + ...advancedTabProps + }) { return ( - { this.renderTabContent(this.renderBasicTabContent()) } +
+ { this.renderBasicTabContent(gasPriceButtonGroupProps) } + { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } +
- { this.renderTabContent(this.renderAdvancedTabContent()) } +
+ { this.renderAdvancedTabContent(advancedTabProps) } + { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } +
) } render () { - const { hideModal } = this.props + const { + hideModal, + infoRowProps, + onSubmit, + customGasPriceInHex, + customGasLimitInHex, + ...tabProps + } = this.props return (
hideModal()} onClose={() => hideModal()} + onSubmit={() => { + onSubmit(customGasLimitInHex, customGasPriceInHex) + hideModal() + }} + submitText={this.context.t('save')} />
) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index ebdd035ea..7e2a7e144 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -1,36 +1,161 @@ import { connect } from 'react-redux' +import { pipe, partialRight } from 'ramda' import GasModalPageContainer from './gas-modal-page-container.component' -import { hideModal } from '../../../actions' +import { + hideModal, + setGasLimit, + setGasPrice, +} from '../../../actions' import { setCustomGasPrice, setCustomGasLimit, } from '../../../ducks/gas.duck' +import { + updateGasAndCalculate, +} from '../../../ducks/confirm-transaction.duck' +import { + getCurrentCurrency, + conversionRateSelector as getConversionRate, + getSelectedToken, +} from '../../../selectors.js' import { getCustomGasPrice, getCustomGasLimit, getRenderableBasicEstimateData, getBasicGasEstimateLoadingStatus, + getAveragePriceEstimateInHexWEI, + getDefaultActiveButtonIndex, } from '../../../selectors/custom-gas' +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, +} from '../../send/send.utils' +import { addHexPrefix } from 'ethereumjs-util' const mapStateToProps = state => { const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) + const { gasPrice, gas: gasLimit, value } = getTxParams(state) + const gasTotal = calcGasTotal(gasLimit, gasPrice) + + const customGasPriceInHex = getCustomGasPrice(state) + const customGasLimitInHex = getCustomGasLimit(state) + const customGasTotal = calcGasTotal(customGasLimitInHex || gasLimit, customGasPriceInHex || gasPrice) + + const gasButtonInfo = getRenderableBasicEstimateData(state) + + const currentCurrency = getCurrentCurrency(state) + const conversionRate = getConversionRate(state) + + const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate) + return { - customGasPrice: getCustomGasPrice(state), - customGasLimit: getCustomGasLimit(state), + isConfirm: isConfirm(state), + customGasPriceInHex, + customGasLimitInHex, + customGasPrice: calcCustomGasPrice(customGasPriceInHex, gasPrice), + customGasLimit: calcCustomGasLimit(customGasLimitInHex, gasLimit), + newTotalFiat, gasPriceButtonGroupProps: { buttonDataLoading, - gasButtonInfo: getRenderableBasicEstimateData(state), + defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customGasPriceInHex, gasPrice), + gasButtonInfo, + }, + infoRowProps: { + originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), + originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), + newTotalFiat, + newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), }, } } const mapDispatchToProps = dispatch => { + const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice))) + return { hideModal: () => dispatch(hideModal()), - updateCustomGasPrice: (newPrice) => dispatch(setCustomGasPrice(newPrice)), - updateCustomGasLimit: (newLimit) => dispatch(setCustomGasLimit(newLimit)), - handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice), + 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) => { + return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) + }, } } -export default connect(mapStateToProps, mapDispatchToProps)(GasModalPageContainer) +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { gasPriceButtonGroupProps, isConfirm } = stateProps + const { + updateCustomGasPrice: dispatchUpdateCustomGasPrice, + setGasData: dispatchSetGasData, + updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, + ...otherDispatchProps + } = dispatchProps + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + onSubmit: isConfirm ? dispatchUpdateConfirmTxGasAndCalculate : dispatchSetGasData, + gasPriceButtonGroupProps: { + ...gasPriceButtonGroupProps, + handleGasPriceSelection: dispatchUpdateCustomGasPrice, + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer) + +function isConfirm (state) { + return Boolean(Object.keys(state.confirmTransaction.txData).length) +} + +function calcCustomGasPrice (customGasPriceInHex, gasPrice) { + return Number(hexWEIToDecGWEI(customGasPriceInHex || gasPrice)) +} + +function calcCustomGasLimit (customGasLimitInHex, gasLimit) { + return parseInt(customGasLimitInHex || gasLimit, 16) +} + +function getTxParams (state) { + const { confirmTransaction: { txData }, metamask: { send } } = state + + return txData.txParams || { + from: send.from, + gas: send.gasLimit, + gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state), + 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) +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 86286b615..c41adca83 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -5,13 +5,12 @@ import sinon from 'sinon' import GasModalPageContainer from '../gas-modal-page-container.component.js' import PageContainer from '../../../page-container' -import BasicTabContent from '../basic-tab-content' -import AdvancedTabContent from '../advanced-tab-content' import { Tab } from '../../../tabs' const propsMethodSpies = { hideModal: sinon.spy(), + onSubmit: sinon.spy(), } const mockGasPriceButtonGroupProps = { @@ -41,18 +40,29 @@ const mockGasPriceButtonGroupProps = { noButtonActiveByDefault: true, showCheck: true, } +const mockInfoRowProps = { + originalTotalFiat: 'mockOriginalTotalFiat', + originalTotalEth: 'mockOriginalTotalEth', + newTotalFiat: 'mockNewTotalFiat', + newTotalEth: 'mockNewTotalEth', +} +const GP = GasModalPageContainer.prototype describe('GasModalPageContainer Component', function () { let wrapper beforeEach(() => { wrapper = shallow( 'mockupdateCustomGasPrice'} updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} customGasPrice={21} customGasLimit={54321} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} + infoRowProps={mockInfoRowProps} + customGasPriceInHex={'mockCustomGasPriceInHex'} + customGasLimitInHex={'mockCustomGasLimitInHex'} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) @@ -89,7 +99,7 @@ describe('GasModalPageContainer Component', function () { }) it('should pass the correct renderTabs property to PageContainer', () => { - sinon.stub(GasModalPageContainer.prototype, 'renderTabs').returns('mockTabs') + sinon.stub(GP, 'renderTabs').returns('mockTabs') const renderTabsWrapperTester = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props() assert.equal(tabsComponent, 'mockTabs') @@ -98,8 +108,23 @@ describe('GasModalPageContainer Component', function () { }) 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() + const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { + gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, + otherProps: 'mockAdvancedTabProps', + }) const renderedTabs = shallow(renderTabsResult) assert.equal(renderedTabs.props().className, 'tabs') @@ -113,45 +138,28 @@ describe('GasModalPageContainer Component', function () { assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content') }) - it('should render the expected children of each tab', () => { - const GP = GasModalPageContainer.prototype - sinon.spy(GP, 'renderTabContent') - assert.equal(GP.renderTabContent.callCount, 0) + 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() + wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) - assert.equal(GP.renderTabContent.callCount, 2) + assert.equal(GP.renderBasicTabContent.callCount, 1) + assert.equal(GP.renderAdvancedTabContent.callCount, 1) - assert.equal(GP.renderTabContent.firstCall.args.type, BasicTabContent.type) - assert.equal(GP.renderTabContent.secondCall.args.type, AdvancedTabContent.type) - - GP.renderTabContent.restore() + assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps) + assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { otherProps: 'mockAdvancedTabProps' }) }) - }) - describe('renderTabContent', () => { - it('should render a root gas-modal-content div', () => { - const renderTabContentResult = wrapper.instance().renderTabContent(() => {}) - const renderedTabContent = shallow(renderTabContentResult) - assert.equal(renderedTabContent.props().className, 'gas-modal-content') - }) - - it('should render the passed element as its first child', () => { - const renderTabContentResult = wrapper.instance().renderTabContent(Mock content) - const renderedTabContent = shallow(renderTabContentResult) - assert(renderedTabContent.childAt(0).equals(Mock content)) - }) + it('should call renderInfoRows with the expected props', () => { + assert.equal(GP.renderInfoRows.callCount, 0) - it('should render the element results of renderInfoRow calls as second and third childs', () => { - const GP = GasModalPageContainer.prototype - sinon.stub(GP, 'renderInfoRow').callsFake((...args) => args.join(',')) + wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) - const renderTabContentResult = wrapper.instance().renderTabContent(() => Mock content) - const renderedTabContent = shallow(renderTabContentResult) - assert.equal(renderedTabContent.childAt(1).text(), 'gas-modal-content__info-row--fade,originalTotal,$20.02 USD,0.06685 ETH') - assert.equal(renderedTabContent.childAt(2).text(), 'gas-modal-content__info-row,newTotal,$20.02 USD,0.06685 ETH') + assert.equal(GP.renderInfoRows.callCount, 2) - GP.renderInfoRow.restore() + assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) + assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) }) }) @@ -176,7 +184,7 @@ describe('GasModalPageContainer Component', function () { describe('renderBasicTabContent', () => { it('should render', () => { - const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent() + const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps) assert.deepEqual( renderBasicTabContentResult.props.gasPriceButtonGroupProps, @@ -187,15 +195,20 @@ describe('GasModalPageContainer Component', function () { describe('renderAdvancedTabContent', () => { it('should render with the correct props', () => { - const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent() + const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({ + convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice', + convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit', + customGasPrice: 123, + customGasLimit: 456, + newTotalFiat: '$0.30', + }) const advancedTabContentProps = renderAdvancedTabContentResult.props - assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockupdateCustomGasPrice') - assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockupdateCustomGasLimit') - assert.equal(advancedTabContentProps.customGasPrice, 21) - assert.equal(advancedTabContentProps.customGasLimit, 54321) + assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') + assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit') + assert.equal(advancedTabContentProps.customGasPrice, 123) + assert.equal(advancedTabContentProps.customGasLimit, 456) assert.equal(advancedTabContentProps.millisecondsRemaining, 91000) assert.equal(advancedTabContentProps.totalFee, '$0.30') - assert(shallow(renderAdvancedTabContentResult).hasClass('advanced-tab')) }) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 4067265b5..e01fd3898 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -7,13 +7,19 @@ let mapDispatchToProps const actionSpies = { hideModal: sinon.spy(), + setGasLimit: sinon.spy(), + setGasPrice: sinon.spy(), } -const customGasActionSpies = { +const gasActionSpies = { setCustomGasPrice: sinon.spy(), setCustomGasLimit: sinon.spy(), } +const confirmTransactionActionSpies = { + updateGasAndCalculate: sinon.spy(), +} + proxyquire('../gas-modal-page-container.container.js', { 'react-redux': { connect: (ms, md) => { @@ -23,13 +29,13 @@ proxyquire('../gas-modal-page-container.container.js', { }, }, '../../../selectors/custom-gas': { - getCustomGasPrice: (s) => `mockGasPrice:${s}`, - getCustomGasLimit: (s) => `mockGasLimit:${s}`, - getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, - getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${s}`, + getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, + getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`, + getDefaultActiveButtonIndex: (a, b, c) => a + b + c, }, '../../../actions': actionSpies, - '../../../ducks/gas.duck': customGasActionSpies, + '../../../ducks/gas.duck': gasActionSpies, + '../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies, }) describe('gas-modal-page-container container', () => { @@ -37,10 +43,54 @@ describe('gas-modal-page-container container', () => { describe('mapStateToProps()', () => { it('should map the correct properties to props', () => { - assert.equal(mapStateToProps('mockState').customGasPrice, 'mockGasPrice:mockState') - assert.equal(mapStateToProps('mockState').customGasLimit, 'mockGasLimit:mockState') - assert.equal(mapStateToProps('mockState').gasPriceButtonGroupProps.buttonDataLoading, 'mockBasicGasEstimateLoadingStatus:mockState') - assert.equal(mapStateToProps('mockState').gasPriceButtonGroupProps.gasButtonInfo, 'mockRenderableBasicEstimateData:mockState') + const mockState2 = { + metamask: { + send: { + gasLimit: '16', + gasPrice: '32', + amount: '64', + }, + currentCurrency: 'abc', + conversionRate: 50, + }, + gas: { + customData: { + limit: 'aaaaaaaa', + price: 'ffffffff', + }, + }, + confirmTransaction: { + txData: { + txParams: { + gas: '0x1600000', + gasPrice: '0x3200000', + value: '0x640000000000000', + }, + }, + }, + } + const result2 = mapStateToProps(mockState2) + + assert.deepEqual(result2, { + isConfirm: true, + customGasPriceInHex: 'ffffffff', + customGasLimitInHex: 'aaaaaaaa', + customGasPrice: 4.294967295, + customGasLimit: 2863311530, + newTotalFiat: '637.41', + gasPriceButtonGroupProps: + { + buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:3', + defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:3ffffffff0x3200000', + gasButtonInfo: 'mockRenderableBasicEstimateData:3', + }, + infoRowProps: { + originalTotalFiat: '22.58', + originalTotalEth: '0.451569 ETH', + newTotalFiat: '637.41', + newTotalEth: '12.748189 ETH', + }, + }) }) }) @@ -54,6 +104,12 @@ describe('gas-modal-page-container container', () => { mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) }) + afterEach(() => { + actionSpies.hideModal.resetHistory() + gasActionSpies.setCustomGasPrice.resetHistory() + gasActionSpies.setCustomGasLimit.resetHistory() + }) + describe('hideModal()', () => { it('should dispatch a hideModal action', () => { mapDispatchToPropsObject.hideModal() @@ -63,18 +119,50 @@ describe('gas-modal-page-container container', () => { }) describe('updateCustomGasPrice()', () => { - it('should dispatch a setCustomGasPrice action', () => { - mapDispatchToPropsObject.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(customGasActionSpies.setCustomGasPrice.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('updateCustomGasLimit()', () => { - it('should dispatch a setCustomGasLimit action', () => { - mapDispatchToPropsObject.updateCustomGasLimit() + describe('updateConfirmTxGasAndCalculate()', () => { + it('should dispatch a updateGasAndCalculate action with the correct props', () => { + mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa') assert(dispatchSpy.calledOnce) - assert(customGasActionSpies.setCustomGasLimit.calledOnce) + assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce) + assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' }) }) }) diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index a3ec82863..1e2270432 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -29,7 +29,7 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { const mapStateToProps = (state, props) => { const { toAddress: propsToAddress } = props - const { confirmTransaction, metamask } = state + const { confirmTransaction, metamask, gas } = state const { ethTransactionAmount, ethTransactionFee, @@ -60,6 +60,12 @@ const mapStateToProps = (state, props) => { unapprovedTxs, } = metamask const assetImage = assetImages[txParamsToAddress] + + const { + customGasLimit, + customGasPrice, + } = gas + const { balance } = accounts[selectedAddress] const { name: fromName } = identities[selectedAddress] const toAddress = propsToAddress || txParamsToAddress @@ -106,6 +112,10 @@ const mapStateToProps = (state, props) => { unapprovedTxs, unapprovedTxCount, currentNetworkUnapprovedTxs, + customGas: { + gasLimit: customGasLimit || txData.gasPrice, + gasPrice: customGasPrice || txData.gasLimit, + }, } } @@ -192,7 +202,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...ownProps, showCustomizeGasModal: () => dispatchShowCustomizeGasModal({ txData, - onSubmit: txData => dispatchUpdateGasAndCalculate(txData), + onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas), validate: validateEditGas, }), cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)), diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js index 3ac656d73..614b3e584 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js @@ -32,6 +32,7 @@ export default class ConfirmTransaction extends Component { setTransactionToConfirm: PropTypes.func, confirmTransaction: PropTypes.object, clearConfirmTransaction: PropTypes.func, + fetchGasEstimates: PropTypes.func, } getParamsTransactionId () { @@ -45,6 +46,7 @@ export default class ConfirmTransaction extends Component { send = {}, history, confirmTransaction: { txData: { id: transactionId } = {} }, + fetchGasEstimates, } = this.props if (!totalUnapprovedCount && !send.to) { @@ -53,6 +55,7 @@ export default class ConfirmTransaction extends Component { } if (!transactionId) { + fetchGasEstimates() this.setTransactionToConfirm() } } diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js index 1bc2f1efb..cb1a737f9 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js @@ -5,6 +5,9 @@ import { setTransactionToConfirm, clearConfirmTransaction, } from '../../../ducks/confirm-transaction.duck' +import { + fetchGasEstimates, +} from '../../../ducks/gas.duck' import ConfirmTransaction from './confirm-transaction.component' import { getTotalUnapprovedCount } from '../../../selectors' import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction' @@ -24,6 +27,7 @@ const mapDispatchToProps = dispatch => { return { setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + fetchGasEstimates: () => dispatch(fetchGasEstimates()), } } diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index a639c24b0..5c12826ea 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -73,10 +73,10 @@ export default class SendTransactionScreen extends PersistentForm { selectedAddress, selectedToken = {}, to: currentToAddress, - updateAndSetGasTotal, + updateAndSetGasLimit, } = this.props - updateAndSetGasTotal({ + updateAndSetGasLimit({ blockGasLimit, editingTransactionId, gasLimit, @@ -164,6 +164,9 @@ export default class SendTransactionScreen extends PersistentForm { componentDidMount () { this.props.fetchGasEstimates() + .then(() => { + this.updateGas() + }) } componentWillMount () { @@ -173,12 +176,12 @@ export default class SendTransactionScreen extends PersistentForm { tokenContract, updateSendTokenBalance, } = this.props + updateSendTokenBalance({ selectedToken, tokenContract, address, }) - this.updateGas() // Show QR Scanner modal if ?scan=true if (window.location.search === '?scan=true') { diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index a00fb3545..f240774d4 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -79,7 +79,7 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - updateAndSetGasTotal: ({ + updateAndSetGasLimit: ({ blockGasLimit, editingTransactionId, gasLimit, @@ -92,7 +92,7 @@ function mapDispatchToProps (dispatch) { data, }) => { !editingTransactionId - ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data })) + ? dispatch(updateGasData({ gasPrice, recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data })) : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) }, updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index c217d848e..71eb93d61 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -8,7 +8,11 @@ const { } = require('../../selectors') const { estimateGasPriceFromRecentBlocks, + calcGasTotal, } = require('./send.utils') +import { + getAveragePriceEstimateInHexWEI, +} from '../../selectors/custom-gas' const selectors = { accountsWithSendEtherInfoSelector, @@ -131,11 +135,11 @@ function getForceGasMin (state) { } function getGasLimit (state) { - return state.metamask.send.gasLimit + return state.metamask.send.gasLimit || '0' } function getGasPrice (state) { - return state.metamask.send.gasPrice + return state.metamask.send.gasPrice || getAveragePriceEstimateInHexWEI(state) } function getGasPriceFromRecentBlocks (state) { @@ -143,7 +147,7 @@ function getGasPriceFromRecentBlocks (state) { } function getGasTotal (state) { - return state.metamask.send.gasTotal + return calcGasTotal(getGasLimit(state), getGasPrice(state)) } function getPrimaryCurrency (state) { diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js index 0f3902c07..82e84de30 100644 --- a/ui/app/components/send/tests/send-component.test.js +++ b/ui/app/components/send/tests/send-component.test.js @@ -9,11 +9,11 @@ import SendContent from '../send-content/send-content.component' import SendFooter from '../send-footer/send-footer.container' const propsMethodSpies = { - updateAndSetGasTotal: sinon.spy(), + updateAndSetGasLimit: sinon.spy(), updateSendErrors: sinon.spy(), updateSendTokenBalance: sinon.spy(), resetSendState: sinon.spy(), - fetchGasEstimates: sinon.spy(), + fetchGasEstimates: sinon.stub().returns(Promise.resolve()), } const utilsMethodStubs = { getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), @@ -52,7 +52,7 @@ describe('Send Component', function () { showHexData={true} tokenBalance={'mockTokenBalance'} tokenContract={'mockTokenContract'} - updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal} + updateAndSetGasLimit={propsMethodSpies.updateAndSetGasLimit} updateSendErrors={propsMethodSpies.updateSendErrors} updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance} resetSendState={propsMethodSpies.resetSendState} @@ -66,7 +66,7 @@ describe('Send Component', function () { utilsMethodStubs.getAmountErrorObject.resetHistory() utilsMethodStubs.getGasFeeErrorObject.resetHistory() propsMethodSpies.fetchGasEstimates.resetHistory() - propsMethodSpies.updateAndSetGasTotal.resetHistory() + propsMethodSpies.updateAndSetGasLimit.resetHistory() propsMethodSpies.updateSendErrors.resetHistory() propsMethodSpies.updateSendTokenBalance.resetHistory() }) @@ -75,16 +75,6 @@ describe('Send Component', function () { assert(SendTransactionScreen.prototype.componentDidMount.calledOnce) }) - describe('componentWillMount', () => { - it('should call this.updateGas', () => { - SendTransactionScreen.prototype.updateGas.resetHistory() - propsMethodSpies.updateSendErrors.resetHistory() - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) - wrapper.instance().componentWillMount() - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) - }) - }) - describe('componentDidMount', () => { it('should call props.fetchGasEstimates', () => { propsMethodSpies.fetchGasEstimates.resetHistory() @@ -92,6 +82,14 @@ describe('Send Component', function () { wrapper.instance().componentDidMount() assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1) }) + + it('should call this.updateGas', () => { + SendTransactionScreen.prototype.updateGas.resetHistory() + propsMethodSpies.updateSendErrors.resetHistory() + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) + wrapper.instance().componentDidMount() + setTimeout(() => assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1), 250) + }) }) describe('componentWillUnmount', () => { @@ -283,12 +281,12 @@ describe('Send Component', function () { }) describe('updateGas', () => { - it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.instance().updateGas() - assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) + assert.equal(propsMethodSpies.updateAndSetGasLimit.callCount, 1) assert.deepEqual( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], + propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0], { blockGasLimit: 'mockBlockGasLimit', editingTransactionId: 'mockEditingTransactionId', @@ -304,20 +302,20 @@ describe('Send Component', function () { ) }) - it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with the correct params if a to prop is passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.setProps({ to: 'someAddress' }) wrapper.instance().updateGas() assert.equal( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, + propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, 'someaddress', ) }) - it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with to set to lowercase if passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.instance().updateGas({ to: '0xABC' }) - assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc') + assert.equal(propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, '0xabc') }) }) diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js index 6aa4bf826..d8fe85d22 100644 --- a/ui/app/components/send/tests/send-container.test.js +++ b/ui/app/components/send/tests/send-container.test.js @@ -94,7 +94,7 @@ describe('send container', () => { mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) }) - describe('updateAndSetGasTotal()', () => { + describe('updateAndSetGasLimit()', () => { const mockProps = { blockGasLimit: 'mockBlockGasLimit', editingTransactionId: '0x2', @@ -109,7 +109,7 @@ describe('send container', () => { } it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => { - mapDispatchToPropsObject.updateAndSetGasTotal(mockProps) + mapDispatchToPropsObject.updateAndSetGasLimit(mockProps) assert(dispatchSpy.calledOnce) assert.equal( actionSpies.setGasTotal.getCall(0).args[0], @@ -118,14 +118,14 @@ describe('send container', () => { }) it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { - const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps - mapDispatchToPropsObject.updateAndSetGasTotal( + const { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps + mapDispatchToPropsObject.updateAndSetGasLimit( Object.assign({}, mockProps, {editingTransactionId: false}) ) assert(dispatchSpy.calledOnce) assert.deepEqual( actionSpies.updateGasData.getCall(0).args[0], - { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } + { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, } ) }) }) diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js index e7e901f0d..cdc86fe59 100644 --- a/ui/app/components/send/tests/send-selectors.test.js +++ b/ui/app/components/send/tests/send-selectors.test.js @@ -237,7 +237,7 @@ describe('send selectors', () => { it('should return the send.gasTotal', () => { assert.equal( getGasTotal(mockState), - '0xb451dc41b578' + 'a9ff56' ) }) }) diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 8b2fbcfdb..7f18b2272 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -13,8 +13,8 @@ const SET_CUSTOM_GAS_TOTAL = 'metamask/gas/SET_CUSTOM_GAS_TOTAL' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { customData: { - price: 0, - limit: 21000, + price: null, + limit: '0x5208', }, basicEstimates: { average: null, diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index 7fd74f815..66cf376a7 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -47,8 +47,8 @@ describe('Gas Duck', () => { } const initState = { customData: { - price: 0, - limit: 21000, + price: null, + limit: '0x5208', }, basicEstimates: { average: null, diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js index cb5e1b90b..d8e0d87fe 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/conversions.util.js @@ -1,6 +1,7 @@ import ethUtil from 'ethereumjs-util' import { conversionUtil } from '../conversion-util' import { ETH, GWEI, WEI } from '../constants/common' +import { conversionUtil, addCurrencies } from '../conversion-util' export function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) @@ -82,3 +83,41 @@ export function getWeiHexFromDecimalValue ({ toDenomination: WEI, }) } + +export function addHexWEIsToDec (aHexWEI, bHexWEI) { + return addCurrencies(aHexWEI, bHexWEI, { + aBase: 16, + bBase: 16, + fromDenomination: 'WEI', + numberOfDecimals: 6, + }) +} + +export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) { + return conversionUtil(ethTotal, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: convertedCurrency, + numberOfDecimals: 2, + conversionRate, + }) +} + +export function decGWEIToHexWEI (decGWEI) { + return conversionUtil(decGWEI, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + }) +} + +export function hexWEIToDecGWEI (decGWEI) { + return conversionUtil(decGWEI, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }) +} diff --git a/ui/app/helpers/formatters.js b/ui/app/helpers/formatters.js new file mode 100644 index 000000000..106a2520d --- /dev/null +++ b/ui/app/helpers/formatters.js @@ -0,0 +1,3 @@ +export function formatETHFee (ethFee) { + return ethFee + ' ETH' +} diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index ff2fd0e05..61c0525df 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -1,8 +1,4 @@ import { pipe, partialRight } from 'ramda' -import { - getConversionRate, - getGasLimit, -} from '../components/send/send.selectors' import { conversionUtil, multiplyCurrencies, @@ -13,6 +9,12 @@ import { 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' @@ -25,6 +27,9 @@ const selectors = { getCustomGasTotal, getRenderableBasicEstimateData, getBasicGasEstimateLoadingStatus, + getAveragePriceEstimateInHexWEI, + getDefaultActiveButtonIndex, + priceEstimateToWei, } module.exports = selectors @@ -49,6 +54,16 @@ function getBasicGasEstimateLoadingStatus (state) { return state.gas.basicEstimateIsLoading } +function getAveragePriceEstimateInHexWEI (state) { + const averagePriceEstimate = state.gas.basicEstimates.average + return getGasPriceInHexWei(averagePriceEstimate || '0x0') +} + +function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { + return gasButtonInfo.findIndex(({ priceInHexWei }) => { + return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice) + }) +} function apiEstimateModifiedToGWEI (estimate) { return multiplyCurrencies(estimate, 0.10, { @@ -68,21 +83,6 @@ function basicPriceEstimateToETHTotal (estimate, gasLimit) { }) } -function ethTotalToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) { - return conversionUtil(ethTotal, { - fromNumericBase: 'dec', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) -} - -function formatETHFee (ethFee) { - return ethFee + ' ETH' -} - function getRenderableEthFee (estimate, gasLimit) { return pipe( apiEstimateModifiedToGWEI, @@ -150,9 +150,8 @@ function getRenderableBasicEstimateData (state) { if (getBasicGasEstimateLoadingStatus(state)) { return [] } - - const gasLimit = getGasLimit(state) - const conversionRate = getConversionRate(state) + const gasLimit = state.metamask.send.gasLimit || getCustomGasLimit(state) + const conversionRate = state.metamask.conversionRate const currentCurrency = getCurrentCurrency(state) const { gas: { From b567c78bcae73e9c73b69040d22e096e4f876a2b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 13 Sep 2018 10:05:17 -0230 Subject: [PATCH 15/46] Integrate gas buttons with the send screen. --- app/_locales/en/messages.json | 12 ++++ .../button-group/button-group.component.js | 6 ++ .../gas-modal-page-container.component.js | 16 +++-- .../gas-modal-page-container.container.js | 13 +++- .../gas-price-button-group.component.js | 27 ++++---- .../gas-price-button-group/index.scss | 67 +++++++++++++++++++ .../page-container.component.js | 3 +- .../gas-fee-display.component.js | 7 +- .../send-gas-row/send-gas-row.component.js | 33 ++++++--- .../send-gas-row/send-gas-row.container.js | 47 +++++++++++-- .../send-gas-row/send-gas-row.selectors.js | 5 ++ ui/app/css/itcss/components/send.scss | 30 ++++++++- ui/app/ducks/send.duck.js | 19 ++++++ ui/app/selectors/custom-gas.js | 57 ++++++++++++++-- 14 files changed, 301 insertions(+), 41 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 017a8a380..7a79da2bd 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -121,6 +121,9 @@ "available": { "message": "Available" }, + "average": { + "message": "Average" + }, "back": { "message": "Back" }, @@ -439,6 +442,9 @@ "failed": { "message": "Failed" }, + "fast": { + "message": "Fast" + }, "feeChartTitle": { "message": "Live Transaction Fee Predictions" }, @@ -1023,6 +1029,9 @@ "save": { "message": "Save" }, + "slow": { + "message": "Slow" + }, "saveAsCsvFile": { "message": "Save as CSV File" }, @@ -1277,6 +1286,9 @@ "transactionErrorNoContract": { "message": "Trying to call a function on a non-contract address." }, + "transactionFee": { + "message": "Transaction Fee" + }, "transactionMemo": { "message": "Transaction memo (optional)" }, diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js index 440b3c4f7..723f9b526 100644 --- a/ui/app/components/button-group/button-group.component.js +++ b/ui/app/components/button-group/button-group.component.js @@ -23,6 +23,12 @@ export default class ButtonGroup extends PureComponent { : this.props.defaultActiveButtonIndex, } + componentDidUpdate (prevProps, prevState) { + if (typeof this.props.newActiveButtonIndex === 'number' && prevState.activeButtonIndex !== this.props.newActiveButtonIndex) { + this.setState({ activeButtonIndex: prevProps.newActiveButtonIndex }) + } + } + handleButtonClick (activeButtonIndex) { this.setState({ activeButtonIndex }) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 89065472d..db730458d 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -89,16 +89,20 @@ export default class GasModalPageContainer extends Component { }, { gasPriceButtonGroupProps, + hideBasic, ...advancedTabProps }) { return ( - -
- { this.renderBasicTabContent(gasPriceButtonGroupProps) } - { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } -
-
+ {hideBasic + ? null + : +
+ { this.renderBasicTabContent(gasPriceButtonGroupProps) } + { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } +
+
+ }
{ this.renderAdvancedTabContent(advancedTabProps) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 7e2a7e144..a150e5009 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -10,6 +10,9 @@ import { setCustomGasPrice, setCustomGasLimit, } from '../../../ducks/gas.duck' +import { + hideGasButtonGroup, +} from '../../../ducks/send.duck' import { updateGasAndCalculate, } from '../../../ducks/confirm-transaction.duck' @@ -59,7 +62,10 @@ const mapStateToProps = state => { const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate) + const hideBasic = state.appState.modal.modalState.props.hideBasic + return { + hideBasic, isConfirm: isConfirm(state), customGasPriceInHex, customGasLimitInHex, @@ -95,6 +101,7 @@ const mapDispatchToProps = dispatch => { updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) }, + hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), } } @@ -102,6 +109,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { const { gasPriceButtonGroupProps, isConfirm } = stateProps const { updateCustomGasPrice: dispatchUpdateCustomGasPrice, + hideGasButtonGroup: dispatchHideGasButtonGroup, setGasData: dispatchSetGasData, updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, ...otherDispatchProps @@ -111,7 +119,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...stateProps, ...otherDispatchProps, ...ownProps, - onSubmit: isConfirm ? dispatchUpdateConfirmTxGasAndCalculate : dispatchSetGasData, + onSubmit: isConfirm ? dispatchUpdateConfirmTxGasAndCalculate : (newLimit, newPrice) => { + dispatchSetGasData(newLimit, newPrice) + dispatchHideGasButtonGroup() + }, gasPriceButtonGroupProps: { ...gasPriceButtonGroupProps, handleGasPriceSelection: dispatchUpdateCustomGasPrice, diff --git a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js index 3873f54bc..8d6675e18 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js +++ b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -27,7 +27,7 @@ export default class GasPriceButtonGroup extends Component { } renderButtonContent ({ - label, + labelKey, feeInPrimaryCurrency, feeInSecondaryCurrency, timeEstimate, @@ -36,7 +36,7 @@ export default class GasPriceButtonGroup extends Component { showCheck, }) { return (
- { label &&
{ label }
} + { labelKey &&
{ this.context.t(labelKey) }
} { feeInPrimaryCurrency &&
{ feeInPrimaryCurrency }
} { feeInSecondaryCurrency &&
{ feeInSecondaryCurrency }
} { timeEstimate &&
{ timeEstimate }
} @@ -57,9 +57,7 @@ export default class GasPriceButtonGroup extends Component { onClick={() => handleGasPriceSelection(priceInHexWei)} key={`gas-price-button-${index}`} > - {buttonDataLoading - ? 'Loading...' - : this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)} + {this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)} ) } @@ -68,18 +66,23 @@ export default class GasPriceButtonGroup extends Component { const { gasButtonInfo, defaultActiveButtonIndex = 1, + newActiveButtonIndex, noButtonActiveByDefault = false, + buttonDataLoading, ...buttonPropsAndFlags } = this.props return ( - - { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } - + !buttonDataLoading + ? + { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } + + :
Loading...
) } } diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/gas-customization/gas-price-button-group/index.scss index e2670edd5..7647c42c0 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/index.scss +++ b/ui/app/components/gas-customization/gas-price-button-group/index.scss @@ -18,6 +18,9 @@ height: 15.4px; } + &__loading-container { + height: 130px; + } .button-group__button, .button-group__button--active { height: 130px; @@ -58,3 +61,67 @@ } } } + +.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; + } + + &__secondary-currency { + font-size: 12px; + } + + &__loading-container { + height: 78px; + } + + .button-group__button, .button-group__button--active { + height: 78px; + background: white; + color: $scorpion; + padding-top: 9px; + padding-left: 8.5px; + + 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 + } + } + } +} diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js index 3a2274a29..672255e27 100644 --- a/ui/app/components/page-container/page-container.component.js +++ b/ui/app/components/page-container/page-container.component.js @@ -58,7 +58,8 @@ export default class PageContainer extends PureComponent { renderActiveTabContent () { const { tabsComponent } = this.props - const { children } = tabsComponent.props + let { children } = tabsComponent.props + children = children.filter(child => child) const { activeTabIndex } = this.state return children[activeTabIndex] diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index 9bbb67506..9962f8028 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -46,11 +46,10 @@ export default class GasFeeDisplay extends Component {
}
) diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js index 6ad4499ff..64a5cd603 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' +import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group' export default class SendGasRow extends Component { @@ -26,21 +27,37 @@ export default class SendGasRow extends Component { gasTotal, gasFeeError, showCustomizeGasModal, + gasPriceButtonGroupProps, + gasButtonGroupShown, + showGasButtonGroup } = this.props return ( - showCustomizeGasModal()} - /> + {gasButtonGroupShown + ?
+ +
showCustomizeGasModal()}> + Advanced Options +
+
+ : showCustomizeGasModal()} + />} +
) } diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js index e44fe4ef1..19e49b391 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js @@ -3,25 +3,64 @@ import { getConversionRate, getCurrentCurrency, getGasTotal, + getGasPrice, } from '../../send.selectors.js' -import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js' -import { showModal } from '../../../../actions' +import{ + getBasicGasEstimateLoadingStatus, + getRenderableEstimateDataForSmallButtons, + getDefaultActiveButtonIndex +} from '../../../../selectors/custom-gas' +import{ + showGasButtonGroup +} from '../../../../ducks/send.duck' +import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js' +import { showModal, setGasPrice } from '../../../../actions' import SendGasRow from './send-gas-row.component' -export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow) function mapStateToProps (state) { + const gasButtonInfo = getRenderableEstimateDataForSmallButtons(state) + const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, getGasPrice(state)) + return { conversionRate: getConversionRate(state), convertedCurrency: getCurrentCurrency(state), gasTotal: getGasTotal(state), gasFeeError: gasFeeIsInError(state), gasLoadingError: getGasLoadingError(state), + gasPriceButtonGroupProps: { + buttonDataLoading: getBasicGasEstimateLoadingStatus(state), + defaultActiveButtonIndex: 1, + newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null, + gasButtonInfo, + }, + gasButtonGroupShown: getGasButtonGroupShown(state), } } function mapDispatchToProps (dispatch) { return { - showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })), + showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })), + setGasPrice: newPrice => dispatch(setGasPrice(newPrice)), + showGasButtonGroup: () => dispatch(showGasButtonGroup()) } } + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { gasPriceButtonGroupProps } = stateProps + const { + setGasPrice: dispatchSetGasPrice, + ...otherDispatchProps + } = dispatchProps + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + gasPriceButtonGroupProps: { + ...gasPriceButtonGroupProps, + handleGasPriceSelection: dispatchSetGasPrice, + }, + } +} \ No newline at end of file diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js index 96f6293c2..79c838543 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js @@ -1,6 +1,7 @@ const selectors = { gasFeeIsInError, getGasLoadingError, + getGasButtonGroupShown, } module.exports = selectors @@ -12,3 +13,7 @@ function getGasLoadingError (state) { function gasFeeIsInError (state) { return Boolean(state.send.errors.gasFee) } + +function getGasButtonGroupShown (state) { + return state.send.gasButtonGroupShown +} diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index c791a24c7..4535ced52 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -579,7 +579,7 @@ font-family: Roboto; font-size: 16px; line-height: 22px; - width: 88px; + width: 112px; font-weight: 400; flex: 0 0 auto; } @@ -622,6 +622,7 @@ &__to-autocomplete { position: relative; + max-width: 260px; &__down-caret { z-index: 1026; @@ -684,6 +685,7 @@ display: flex; align-items: center; } + } &__sliders-icon-container { @@ -917,6 +919,15 @@ display: none; } } + +} + +.advanced-gas-options-btn { + display: flex; + justify-content: flex-end; + font-size: 14px; + color: #2f9ae0; + cursor: pointer; } .sliders-icon-container { @@ -935,6 +946,23 @@ font-size: 1em; } +.gas-fee-reset { + display: flex; + align-items: center; + justify-content: center; + height: 24px; + border-radius: 4px; + background-color: #fff; + position: absolute; + right: 12px; + top: 14px; + cursor: pointer; + font-size: 1em; + font-size: 14px; + color: #2f9ae0; + font-family: Roboto; +} + .sliders-icon { color: $curious-blue; } diff --git a/ui/app/ducks/send.duck.js b/ui/app/ducks/send.duck.js index db01bbaa9..758916d48 100644 --- a/ui/app/ducks/send.duck.js +++ b/ui/app/ducks/send.duck.js @@ -7,11 +7,14 @@ const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN' const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN' const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS' const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE' +const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP' +const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { fromDropdownOpen: false, toDropdownOpen: false, + gasButtonGroupShown: true, errors: {}, } @@ -43,6 +46,14 @@ export default function reducer ({ send: sendState = initState }, action = {}) { ...action.value, }, }) + case SHOW_GAS_BUTTON_GROUP: + return extend(newState, { + gasButtonGroupShown: true, + }) + case HIDE_GAS_BUTTON_GROUP: + return extend(newState, { + gasButtonGroupShown: false, + }) case RESET_SEND_STATE: return extend({}, initState) default: @@ -67,6 +78,14 @@ export function closeToDropdown () { return { type: CLOSE_TO_DROPDOWN } } +export function showGasButtonGroup () { + return { type: SHOW_GAS_BUTTON_GROUP } +} + +export function hideGasButtonGroup () { + return { type: HIDE_GAS_BUTTON_GROUP } +} + export function updateSendErrors (errorObject) { return { type: UPDATE_SEND_ERRORS, diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 61c0525df..f023b03b8 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -25,6 +25,7 @@ const selectors = { getCustomGasLimit, getCustomGasPrice, getCustomGasTotal, + getRenderableEstimateDataForSmallButtons, getRenderableBasicEstimateData, getBasicGasEstimateLoadingStatus, getAveragePriceEstimateInHexWEI, @@ -34,6 +35,8 @@ const selectors = { module.exports = selectors +const NUMBER_OF_DECIMALS_SM_BTNS = 5 + function getCustomGasErrors (state) { return state.gas.errors } @@ -60,7 +63,9 @@ function getAveragePriceEstimateInHexWEI (state) { } function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { + console.log('gasButtonInfo', gasButtonInfo) return gasButtonInfo.findIndex(({ priceInHexWei }) => { + console.log('priceInHexWei', priceInHexWei, '|', customGasPriceInHex) return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice) }) } @@ -74,23 +79,24 @@ function apiEstimateModifiedToGWEI (estimate) { }) } -function basicPriceEstimateToETHTotal (estimate, gasLimit) { +function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) { return conversionUtil(calcGasTotal(gasLimit, estimate), { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'GWEI', - numberOfDecimals: 9, + numberOfDecimals, }) } -function getRenderableEthFee (estimate, gasLimit) { +function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) { return pipe( apiEstimateModifiedToGWEI, - partialRight(basicPriceEstimateToETHTotal, [gasLimit]), + partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]), formatETHFee )(estimate, gasLimit) } + function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) { return pipe( apiEstimateModifiedToGWEI, @@ -188,3 +194,46 @@ function getRenderableBasicEstimateData (state) { }, ] } + +function getRenderableEstimateDataForSmallButtons (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, + average, + fast, + blockTime, + safeLowWait, + avgWait, + fastWait, + }, + }, + } = state + + return [ + { + labelKey: 'fast', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS), + priceInHexWei: getGasPriceInHexWei(fast), + }, + { + labelKey: 'average', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(average, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS), + priceInHexWei: getGasPriceInHexWei(average), + }, + { + labelKey: 'slow', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate), + feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS), + priceInHexWei: getGasPriceInHexWei(safeLow), + }, + ] +} From 5354325fab9b9ab3091e3c49e6b940fa713d1799 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 20 Sep 2018 01:46:43 -0230 Subject: [PATCH 16/46] Test updates and additions for button integration with send screen. --- app/_locales/en/messages.json | 3 + .../button-group/button-group.component.js | 6 +- .../tests/button-group-component.test.js | 14 +++ .../gas-modal-page-container.component.js | 23 ++-- .../gas-modal-page-container.container.js | 10 +- ...gas-modal-page-container-component.test.js | 13 ++ ...gas-modal-page-container-container.test.js | 114 +++++++++++++++++- .../gas-price-button-group.component.js | 3 +- .../gas-price-button-group-component.test.js | 35 +++--- .../gas-fee-display.component.js | 4 +- .../test/gas-fee-display.component.test.js | 10 +- .../send-gas-row/send-gas-row.component.js | 9 +- .../send-gas-row/send-gas-row.container.js | 12 +- .../tests/send-gas-row-component.test.js | 37 +++++- .../tests/send-gas-row-container.test.js | 81 ++++++++++++- .../tests/send-gas-row-selectors.test.js | 13 ++ ui/app/ducks/tests/send-duck.test.js | 37 ++++++ ui/app/selectors/custom-gas.js | 6 - ui/app/selectors/tests/custom-gas.test.js | 99 +++++++++++++++ 19 files changed, 465 insertions(+), 64 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7a79da2bd..1fd4c8e1e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -65,6 +65,9 @@ "address": { "message": "Address" }, + "advancedOptions": { + "message": "Advanced Options" + }, "addCustomToken": { "message": "Add custom token" }, diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js index 723f9b526..17a281030 100644 --- a/ui/app/components/button-group/button-group.component.js +++ b/ui/app/components/button-group/button-group.component.js @@ -10,6 +10,7 @@ export default class ButtonGroup extends PureComponent { children: PropTypes.array, className: PropTypes.string, style: PropTypes.object, + newActiveButtonIndex: PropTypes.number, } static defaultProps = { @@ -23,9 +24,10 @@ export default class ButtonGroup extends PureComponent { : this.props.defaultActiveButtonIndex, } - componentDidUpdate (prevProps, prevState) { + componentDidUpdate (_, prevState) { + // Provides an API for dynamically updating the activeButtonIndex if (typeof this.props.newActiveButtonIndex === 'number' && prevState.activeButtonIndex !== this.props.newActiveButtonIndex) { - this.setState({ activeButtonIndex: prevProps.newActiveButtonIndex }) + this.setState({ activeButtonIndex: this.props.newActiveButtonIndex }) } } diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js index f07bb97c8..0bece90d6 100644 --- a/ui/app/components/button-group/tests/button-group-component.test.js +++ b/ui/app/components/button-group/tests/button-group-component.test.js @@ -35,6 +35,20 @@ describe('ButtonGroup Component', function () { ButtonGroup.prototype.renderButtons.resetHistory() }) + describe('componentDidUpdate', () => { + it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.setProps({ newActiveButtonIndex: 2 }) + assert.equal(wrapper.state('activeButtonIndex'), 2) + }) + + it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.setProps({ newActiveButtonIndex: null }) + assert.equal(wrapper.state('activeButtonIndex'), 1) + }) + }) + describe('handleButtonClick', () => { it('should set the activeButtonIndex', () => { assert.equal(wrapper.state('activeButtonIndex'), 1) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index db730458d..41fe901fa 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -92,23 +92,24 @@ export default class GasModalPageContainer extends Component { hideBasic, ...advancedTabProps }) { + let tabsToRender = [ + { name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) }, + { name: 'advanced', content: this.renderAdvancedTabContent(advancedTabProps) }, + ] + + if (hideBasic) { + tabsToRender = tabsToRender.slice(1) + } + return ( - {hideBasic - ? null - : + {tabsToRender.map(({ name, content }, i) =>
- { this.renderBasicTabContent(gasPriceButtonGroupProps) } + { content } { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) }
- } - -
- { this.renderAdvancedTabContent(advancedTabProps) } - { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } -
-
+ )}
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index a150e5009..f9ed1cebb 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -119,10 +119,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...stateProps, ...otherDispatchProps, ...ownProps, - onSubmit: isConfirm ? dispatchUpdateConfirmTxGasAndCalculate : (newLimit, newPrice) => { - dispatchSetGasData(newLimit, newPrice) - dispatchHideGasButtonGroup() - }, + onSubmit: isConfirm + ? dispatchUpdateConfirmTxGasAndCalculate + : (newLimit, newPrice) => { + dispatchSetGasData(newLimit, newPrice) + dispatchHideGasButtonGroup() + }, gasPriceButtonGroupProps: { ...gasPriceButtonGroupProps, handleGasPriceSelection: dispatchUpdateCustomGasPrice, diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index c41adca83..fdd7709d9 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -161,6 +161,19 @@ describe('GasModalPageContainer Component', function () { assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) }) + + 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('renderInfoRow', () => { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index e01fd3898..238f27ed6 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -4,6 +4,7 @@ import sinon from 'sinon' let mapStateToProps let mapDispatchToProps +let mergeProps const actionSpies = { hideModal: sinon.spy(), @@ -20,11 +21,16 @@ const confirmTransactionActionSpies = { updateGasAndCalculate: sinon.spy(), } +const sendActionSpies = { + hideGasButtonGroup: sinon.spy(), +} + proxyquire('../gas-modal-page-container.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms, md, mp) => { mapStateToProps = ms mapDispatchToProps = md + mergeProps = mp return () => ({}) }, }, @@ -36,6 +42,7 @@ proxyquire('../gas-modal-page-container.container.js', { '../../../actions': actionSpies, '../../../ducks/gas.duck': gasActionSpies, '../../../ducks/confirm-transaction.duck': confirmTransactionActionSpies, + '../../../ducks/send.duck': sendActionSpies, }) describe('gas-modal-page-container container', () => { @@ -44,6 +51,15 @@ describe('gas-modal-page-container container', () => { it('should map the correct properties to props', () => { const mockState2 = { + appState: { + modal: { + modalState: { + props: { + hideBasic: true, + }, + }, + }, + }, metamask: { send: { gasLimit: '16', @@ -80,10 +96,11 @@ describe('gas-modal-page-container container', () => { newTotalFiat: '637.41', gasPriceButtonGroupProps: { - buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:3', - defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:3ffffffff0x3200000', - gasButtonInfo: 'mockRenderableBasicEstimateData:3', + buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', + defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff0x3200000', + gasButtonInfo: 'mockRenderableBasicEstimateData:4', }, + hideBasic: true, infoRowProps: { originalTotalFiat: '22.58', originalTotalEth: '0.451569 ETH', @@ -110,6 +127,14 @@ describe('gas-modal-page-container container', () => { gasActionSpies.setCustomGasLimit.resetHistory() }) + describe('hideGasButtonGroup()', () => { + it('should dispatch a hideGasButtonGroup action', () => { + mapDispatchToPropsObject.hideGasButtonGroup() + assert(dispatchSpy.calledOnce) + assert(sendActionSpies.hideGasButtonGroup.calledOnce) + }) + }) + describe('hideModal()', () => { it('should dispatch a hideModal action', () => { mapDispatchToPropsObject.hideModal() @@ -168,4 +193,85 @@ describe('gas-modal-page-container container', () => { }) + 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(), + } + ownProps = { someOwnProp: 123 } + }) + 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) + + result.onSubmit() + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + + 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) + + 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.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) + }) + }) + }) diff --git a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js index 8d6675e18..62ebb512d 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js +++ b/ui/app/components/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -22,6 +22,7 @@ export default class GasPriceButtonGroup extends Component { defaultActiveButtonIndex: PropTypes.number, gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)), handleGasPriceSelection: PropTypes.func, + newActiveButtonIndex: PropTypes.number, noButtonActiveByDefault: PropTypes.bool, showCheck: PropTypes.bool, } @@ -82,7 +83,7 @@ export default class GasPriceButtonGroup extends Component { > { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } - :
Loading...
+ :
{ this.context.t('loading') }
) } } diff --git a/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js index e1458188d..79f74f8e4 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js +++ b/ui/app/components/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js @@ -1,6 +1,6 @@ import React from 'react' import assert from 'assert' -import { shallow } from 'enzyme' +import shallow from '../../../../../lib/shallow-with-context' import sinon from 'sinon' import GasPriceButtonGroup from '../gas-price-button-group.component' @@ -36,7 +36,6 @@ const mockGasPriceButtonGroupProps = { } const mockButtonPropsAndFlags = Object.assign({}, { - buttonDataLoading: mockGasPriceButtonGroupProps.buttonDataLoading, className: mockGasPriceButtonGroupProps.className, handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection, showCheck: mockGasPriceButtonGroupProps.showCheck, @@ -87,12 +86,19 @@ describe('GasPriceButtonGroup Component', function () { ) } - it('should called this.renderButton 3 times, with the correct args', () => { + 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', () => { @@ -147,29 +153,18 @@ describe('GasPriceButtonGroup Component', function () { ] ) }) - - it('should not call renderButtonContent if buttonDataLoading is true and should show a loading indicator', () => { - GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() - const renderButtonResult = GasPriceButtonGroup.prototype.renderButton( - Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]), - Object.assign({}, mockButtonPropsAndFlags, {buttonDataLoading: true}) - ) - wrappedRenderButtonResult = shallow(renderButtonResult) - assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 0) - assert.equal(wrappedRenderButtonResult.childAt(0).text(), 'Loading...') - }) }) describe('renderButtonContent', () => { - it('should render a label if passed a label', () => { - const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ - label: 'mockLabel', + 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(), 'mockLabel') + assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey') }) it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => { @@ -215,8 +210,8 @@ describe('GasPriceButtonGroup Component', function () { }) it('should render all elements if all args passed', () => { - const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ - label: 'mockLabel', + const renderButtonContentResult = wrapper.instance().renderButtonContent({ + labelKey: 'mockLabel', feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', timeEstimate: 'mockTimeEstimate', diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index 9962f8028..40f8b1fe0 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -11,7 +11,7 @@ export default class GasFeeDisplay extends Component { convertedCurrency: PropTypes.string, gasLoadingError: PropTypes.bool, gasTotal: PropTypes.string, - onClick: PropTypes.func, + showGasButtonGroup: PropTypes.func, }; static contextTypes = { @@ -49,7 +49,7 @@ export default class GasFeeDisplay extends Component { className="gas-fee-reset" onClick={showGasButtonGroup} > - Reset + { this.context.t('reset') }
) diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index 9ff01493a..c7ac175d9 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -17,9 +17,9 @@ describe('SendGasRow Component', function () { wrapper = shallow(, {context: {t: str => str + '_t'}}) }) @@ -41,13 +41,19 @@ describe('SendGasRow Component', function () { assert.equal(value, 'mockGasTotal') }) - it('should render the Button with the correct props', () => { + it('should render the reset button with the correct props', () => { const { onClick, + className, } = wrapper.find('button').props() + assert.equal(className, 'gas-fee-reset') assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) onClick() assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) }) + + it('should render the reset button with the correct text', () => { + assert.equal(wrapper.find('button').text(), 'reset_t') + }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js index 64a5cd603..507407306 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js @@ -13,6 +13,9 @@ export default class SendGasRow extends Component { gasLoadingError: PropTypes.bool, gasTotal: PropTypes.string, showCustomizeGasModal: PropTypes.func, + gasPriceButtonGroupProps: PropTypes.object, + showGasButtonGroup: PropTypes.func, + gasButtonGroupShown: PropTypes.bool, } static contextTypes = { @@ -29,7 +32,7 @@ export default class SendGasRow extends Component { showCustomizeGasModal, gasPriceButtonGroupProps, gasButtonGroupShown, - showGasButtonGroup + showGasButtonGroup, } = this.props return ( @@ -43,10 +46,10 @@ export default class SendGasRow extends Component {
showCustomizeGasModal()}> - Advanced Options + { this.context.t('advancedOptions') }
: dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })), setGasPrice: newPrice => dispatch(setGasPrice(newPrice)), - showGasButtonGroup: () => dispatch(showGasButtonGroup()) + showGasButtonGroup: () => dispatch(showGasButtonGroup()), } } @@ -63,4 +63,4 @@ function mergeProps (stateProps, dispatchProps, ownProps) { handleGasPriceSelection: dispatchSetGasPrice, }, } -} \ No newline at end of file +} diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js index 54a92bd2d..171cd7bf6 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -6,6 +6,7 @@ import SendGasRow from '../send-gas-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component' +import GasPriceButtonGroup from '../../../../gas-customization/gas-price-button-group' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), @@ -21,7 +22,13 @@ describe('SendGasRow Component', function () { gasFeeError={'mockGasFeeError'} gasLoadingError={false} gasTotal={'mockGasTotal'} + showGasButtonGroup={'mockShowGasPriceButtonGroup'} + gasButtonGroupShown={false} showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal} + gasPriceButtonGroupProps={{ + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }} />, { context: { t: str => str + '_t' } }) }) @@ -41,7 +48,7 @@ describe('SendGasRow Component', function () { errorType, } = wrapper.find(SendRowWrapper).props() - assert.equal(label, 'gasFee_t:') + assert.equal(label, 'transactionFee_t:') assert.equal(showError, 'mockGasFeeError') assert.equal(errorType, 'gasFee') }) @@ -57,14 +64,42 @@ describe('SendGasRow Component', function () { gasLoadingError, gasTotal, onClick, + showGasButtonGroup, } = wrapper.find(SendRowWrapper).childAt(0).props() assert.equal(conversionRate, 20) assert.equal(convertedCurrency, 'mockConvertedCurrency') assert.equal(gasLoadingError, false) assert.equal(gasTotal, 'mockGasTotal') + assert.equal(showGasButtonGroup, 'mockShowGasPriceButtonGroup') assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) onClick() assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) }) + + it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => { + wrapper.setProps({ gasButtonGroupShown: true }) + const rendered = wrapper.find(SendRowWrapper).childAt(0) + assert.equal(rendered.children().length, 2) + + const gasPriceButtonGroup = rendered.childAt(0) + assert(gasPriceButtonGroup.is(GasPriceButtonGroup)) + assert(gasPriceButtonGroup.hasClass('gas-price-button-group--small')) + assert.equal(gasPriceButtonGroup.props().showCheck, false) + assert.equal(gasPriceButtonGroup.props().someGasPriceButtonGroupProp, 'foo') + assert.equal(gasPriceButtonGroup.props().anotherGasPriceButtonGroupProp, 'bar') + }) + + it('should render an advanced options button if gasButtonGroupShown is true', () => { + wrapper.setProps({ gasButtonGroupShown: true }) + const rendered = wrapper.find(SendRowWrapper).childAt(0) + assert.equal(rendered.children().length, 2) + + const advancedOptionsButton = rendered.childAt(1) + assert.equal(advancedOptionsButton.text(), 'advancedOptions_t') + + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + advancedOptionsButton.props().onClick() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js index 2ce062505..1c385e216 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -4,16 +4,23 @@ import sinon from 'sinon' let mapStateToProps let mapDispatchToProps +let mergeProps const actionSpies = { showModal: sinon.spy(), + setGasPrice: sinon.spy(), +} + +const sendDuckSpies = { + showGasButtonGroup: sinon.spy(), } proxyquire('../send-gas-row.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms, md, mp) => { mapStateToProps = ms mapDispatchToProps = md + mergeProps = mp return () => ({}) }, }, @@ -21,12 +28,20 @@ proxyquire('../send-gas-row.container.js', { getConversionRate: (s) => `mockConversionRate:${s}`, getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`, + getGasPrice: (s) => `mockGasPrice:${s}`, }, './send-gas-row.selectors.js': { getGasLoadingError: (s) => `mockGasLoadingError:${s}`, gasFeeIsInError: (s) => `mockGasFeeError:${s}`, + getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`, }, '../../../../actions': actionSpies, + '../../../../selectors/custom-gas': { + getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, + getRenderableEstimateDataForSmallButtons: (s) => `mockGasButtonInfo:${s}`, + getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length, + }, + '../../../../ducks/send.duck': sendDuckSpies, }) describe('send-gas-row container', () => { @@ -40,6 +55,13 @@ describe('send-gas-row container', () => { gasTotal: 'mockGasTotal:mockState', gasFeeError: 'mockGasFeeError:mockState', gasLoadingError: 'mockGasLoadingError:mockState', + gasPriceButtonGroupProps: { + buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`, + defaultActiveButtonIndex: 1, + newActiveButtonIndex: 49, + gasButtonInfo: `mockGasButtonInfo:mockState`, + }, + gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`, }) }) @@ -60,11 +82,66 @@ describe('send-gas-row container', () => { assert(dispatchSpy.calledOnce) assert.deepEqual( actionSpies.showModal.getCall(0).args[0], - { name: 'CUSTOMIZE_GAS' } + { name: 'CUSTOMIZE_GAS', hideBasic: true } ) }) }) + describe('setGasPrice()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setGasPrice('mockNewPrice') + assert(dispatchSpy.calledOnce) + assert(actionSpies.setGasPrice.calledOnce) + assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice') + }) + }) + + describe('showGasButtonGroup()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.showGasButtonGroup() + assert(dispatchSpy.calledOnce) + assert(sendDuckSpies.showGasButtonGroup.calledOnce) + }) + }) + + }) + + describe('mergeProps', () => { + let stateProps + let dispatchProps + let ownProps + + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + someOtherStateProp: 'baz', + } + dispatchProps = { + setGasPrice: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + } + ownProps = { someOwnProp: 123 } + }) + + it('should return the expected props when isConfirm is true', () => { + const result = mergeProps(stateProps, dispatchProps, ownProps) + + 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.setGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.setGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js index d46dd9d8b..bd3c9a257 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js @@ -2,6 +2,7 @@ import assert from 'assert' import { gasFeeIsInError, getGasLoadingError, + getGasButtonGroupShown, } from '../send-gas-row.selectors.js' describe('send-gas-row selectors', () => { @@ -46,4 +47,16 @@ describe('send-gas-row selectors', () => { }) }) + describe('getGasButtonGroupShown()', () => { + it('should return send.gasButtonGroupShown', () => { + const state = { + send: { + gasButtonGroupShown: 'foobar', + }, + } + + assert.equal(getGasButtonGroupShown(state), 'foobar') + }) + }) + }) diff --git a/ui/app/ducks/tests/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js index c101132d9..196fe226c 100644 --- a/ui/app/ducks/tests/send-duck.test.js +++ b/ui/app/ducks/tests/send-duck.test.js @@ -6,6 +6,8 @@ import SendReducer, { openToDropdown, closeToDropdown, updateSendErrors, + showGasButtonGroup, + hideGasButtonGroup, } from '../send.duck.js' describe('Send Duck', () => { @@ -18,6 +20,7 @@ describe('Send Duck', () => { fromDropdownOpen: false, toDropdownOpen: false, errors: {}, + gasButtonGroupShown: true, } const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN' const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN' @@ -25,6 +28,8 @@ describe('Send Duck', () => { const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN' const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS' const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE' + const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP' + const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP' describe('SendReducer()', () => { it('should initialize state', () => { @@ -85,6 +90,24 @@ describe('Send Duck', () => { ) }) + it('should set gasButtonGroupShown to true when receiving a SHOW_GAS_BUTTON_GROUP action', () => { + assert.deepEqual( + SendReducer(Object.assign({}, mockState, { gasButtonGroupShown: false }), { + type: SHOW_GAS_BUTTON_GROUP, + }), + Object.assign({gasButtonGroupShown: true}, mockState.send) + ) + }) + + it('should set gasButtonGroupShown to false when receiving a HIDE_GAS_BUTTON_GROUP action', () => { + assert.deepEqual( + SendReducer(mockState, { + type: HIDE_GAS_BUTTON_GROUP, + }), + Object.assign({gasButtonGroupShown: false}, mockState.send) + ) + }) + it('should extend send.errors with the value of a UPDATE_SEND_ERRORS action', () => { const modifiedMockState = Object.assign({}, mockState, { send: { @@ -145,6 +168,20 @@ describe('Send Duck', () => { ) }) + describe('showGasButtonGroup', () => { + assert.deepEqual( + showGasButtonGroup(), + { type: SHOW_GAS_BUTTON_GROUP } + ) + }) + + describe('hideGasButtonGroup', () => { + assert.deepEqual( + hideGasButtonGroup(), + { type: HIDE_GAS_BUTTON_GROUP } + ) + }) + describe('updateSendErrors', () => { assert.deepEqual( updateSendErrors('mockErrorObject'), diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index f023b03b8..d5a3972df 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -63,9 +63,7 @@ function getAveragePriceEstimateInHexWEI (state) { } function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { - console.log('gasButtonInfo', gasButtonInfo) return gasButtonInfo.findIndex(({ priceInHexWei }) => { - console.log('priceInHexWei', priceInHexWei, '|', customGasPriceInHex) return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice) }) } @@ -208,10 +206,6 @@ function getRenderableEstimateDataForSmallButtons (state) { safeLow, average, fast, - blockTime, - safeLowWait, - avgWait, - fastWait, }, }, } = state diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js index d53c1ec98..5fde61c9b 100644 --- a/ui/app/selectors/tests/custom-gas.test.js +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -7,6 +7,7 @@ const { getCustomGasPrice, getCustomGasTotal, getRenderableBasicEstimateData, + getRenderableEstimateDataForSmallButtons, } = proxyquire('../custom-gas', {}) describe('custom-gas selectors', () => { @@ -137,4 +138,102 @@ describe('custom-gas selectors', () => { }) + describe('getRenderableEstimateDataForSmallButtons()', () => { + const tests = [ + { + expectedResult: [ + { + feeInSecondaryCurrency: '$0.05', + feeInPrimaryCurrency: '0.00021 ETH', + labelKey: 'fast', + priceInHexWei: '0x2540be400', + }, + { + feeInSecondaryCurrency: '$0.03', + feeInPrimaryCurrency: '0.0001 ETH', + labelKey: 'average', + priceInHexWei: '0x12a05f200', + }, + { + feeInSecondaryCurrency: '$0.01', + feeInPrimaryCurrency: '0.00005 ETH', + labelKey: 'slow', + priceInHexWei: '0x9502f900', + }, + ], + mockState: { + metamask: { + conversionRate: 255.71, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 25, + safeLowWait: 6.6, + average: 50, + avgWait: 3.3, + fast: 100, + fastWait: 0.5, + }, + }, + }, + }, + { + expectedResult: [ + { + feeInSecondaryCurrency: '$1.07', + feeInPrimaryCurrency: '0.00042 ETH', + labelKey: 'fast', + priceInHexWei: '0x4a817c800', + }, + { + feeInSecondaryCurrency: '$0.54', + feeInPrimaryCurrency: '0.00021 ETH', + labelKey: 'average', + priceInHexWei: '0x2540be400', + }, + { + feeInSecondaryCurrency: '$0.27', + feeInPrimaryCurrency: '0.0001 ETH', + labelKey: 'slow', + priceInHexWei: '0x12a05f200', + }, + ], + mockState: { + metamask: { + conversionRate: 2557.1, + currentCurrency: 'usd', + send: { + gasLimit: '0x5208', + }, + }, + gas: { + basicEstimates: { + blockTime: 14.16326530612245, + safeLow: 50, + safeLowWait: 13.2, + average: 100, + avgWait: 6.6, + fast: 200, + fastWait: 1.0, + }, + }, + }, + }, + ] + it('should return renderable data about basic estimates appropriate for buttons with less info', () => { + tests.forEach(test => { + assert.deepEqual( + getRenderableEstimateDataForSmallButtons(test.mockState), + test.expectedResult + ) + }) + }) + + }) + }) From b95eb30ec60e4d169a61d987ad86fe333aa49523 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 20 Sep 2018 13:36:23 -0230 Subject: [PATCH 17/46] Adds redesign for the customize gas advanced tab. --- .../advanced-tab-content.component.js | 35 ++++------ .../advanced-tab-content/index.scss | 44 ++++++++---- .../advanced-tab-content-component.test.js | 56 +++++++++------ .../time-remaining/index.scss | 8 ++- .../basic-tab-content/index.scss | 1 + .../gas-modal-page-container.component.js | 63 +++++++++-------- .../gas-modal-page-container.container.js | 36 ++++++---- .../gas-modal-page-container/index.scss | 68 +++++++++++++++++-- ...gas-modal-page-container-component.test.js | 67 ++++++++++-------- ...gas-modal-page-container-container.test.js | 21 +++--- .../gas-price-chart.component.js | 16 +++++ .../gas-price-chart/index.js | 1 + .../gas-price-chart/index.scss | 6 ++ .../tests/gas-price-chart.component.test.js | 25 +++++++ .../components/gas-customization/index.scss | 2 + .../page-container-footer.component.js | 6 +- .../page-container-header.component.js | 13 ++-- .../page-container.component.js | 6 ++ ui/app/ducks/gas.duck.js | 10 +++ 19 files changed, 332 insertions(+), 152 deletions(-) create mode 100644 ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js create mode 100644 ui/app/components/gas-customization/gas-price-chart/index.js create mode 100644 ui/app/components/gas-customization/gas-price-chart/index.scss create mode 100644 ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 56d10cc2b..5218dd477 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import TimeRemaining from './time-remaining' +import GasPriceChart from '../../gas-price-chart' export default class AdvancedTabContent extends Component { static contextTypes = { @@ -14,6 +14,7 @@ export default class AdvancedTabContent extends Component { customGasLimit: PropTypes.number, millisecondsRemaining: PropTypes.number, totalFee: PropTypes.string, + timeRemaining: PropTypes.string, } gasInput (value, onChange, min, precision, showGWEI) { @@ -27,9 +28,6 @@ export default class AdvancedTabContent extends Component { precision={precision} onChange={event => onChange(Number(event.target.value))} /> - {showGWEI - ? GWEI - : null}
) } @@ -38,7 +36,7 @@ export default class AdvancedTabContent extends Component { return } - renderDataSummary (totalFee, millisecondsRemaining) { + renderDataSummary (totalFee, timeRemaining) { return (
@@ -49,9 +47,7 @@ export default class AdvancedTabContent extends Component {
{totalFee}
- +
{timeRemaining}
) @@ -72,7 +68,7 @@ export default class AdvancedTabContent extends Component { renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit) { return (
- { this.renderGasEditRow('gasPriceNoDenom', customGasPrice, updateCustomGasPrice, customGasPrice, 9, true) } + { this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, 9, true) } { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, customGasLimit, 0) }
) @@ -82,7 +78,7 @@ export default class AdvancedTabContent extends Component { const { updateCustomGasPrice, updateCustomGasLimit, - millisecondsRemaining, + timeRemaining, customGasPrice, customGasLimit, totalFee, @@ -90,17 +86,16 @@ export default class AdvancedTabContent extends Component { return (
- { this.renderDataSummary(totalFee, millisecondsRemaining) } -
- { this.context.t('feeChartTitle') } + { this.renderDataSummary(totalFee, timeRemaining) } +
+ { this.renderGasEditRows( + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit + ) } +
-
- { this.renderGasEditRows( - customGasPrice, - updateCustomGasPrice, - customGasLimit, - updateCustomGasLimit - ) }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index aced75449..ae99ba4aa 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -3,11 +3,9 @@ .advanced-tab { display: flex; flex-flow: column; - height: 430px; &__transaction-data-summary, - &__fee-chart-title, - &__gas-edit-row { + &__fee-chart-title { padding-left: 24px; padding-right: 24px; } @@ -17,6 +15,8 @@ flex-flow: column; color: $mid-gray; margin-top: 12px; + padding-left: 18px; + padding-right: 18px; &__titles, &__container { @@ -24,11 +24,17 @@ flex-flow: row; justify-content: space-between; font-size: 12px; + color: #888EA3; } &__container { - font-size: 26px; - margin-top: 6px; + font-size: 16px; + margin-top: 0px; + } + + &__fee { + font-size: 16px; + color: #313A5E; } } @@ -40,8 +46,11 @@ &__fee-chart { padding-left: 10px; - margin-top: 24px; - height: 134px; + margin-top: 8px; + height: 258px; + background: #F8F9FB; + border-bottom: 1px solid #d2d8dd; + border-top: 1px solid #d2d8dd; } &__slider-container { @@ -50,21 +59,25 @@ } &__gas-edit-rows { - margin-top: 44px; height: 87px; display: flex; - flex-flow: column; + flex-flow: row; justify-content: space-between; + margin-left: 10px; + margin-right: 10px; + margin-top: 9px; } &__gas-edit-row { display: flex; - flex-flow: row; - justify-content: space-between; + flex-flow: column; &__label { - color: $mid-gray; - font-size: 16px; + color: #313B5E; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; .fa-info-circle { color: $silver; @@ -87,10 +100,11 @@ border-radius: 4px; color: $mid-gray; font-size: 16px; - height: 37px; - width: 163px; + height: 24px; + width: 155px; padding-left: 8px; padding-top: 2px; + margin-top: 7px; } input[type="number"]::-webkit-inner-spin-button { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 0ef286b8a..1489c7696 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -4,7 +4,7 @@ import shallow from '../../../../../../lib/shallow-with-context' import sinon from 'sinon' import AdvancedTabContent from '../advanced-tab-content.component.js' -import TimeRemaining from '../time-remaining' +import GasPriceChart from '../../../gas-price-chart' const propsMethodSpies = { updateCustomGasPrice: sinon.spy(), @@ -13,6 +13,8 @@ const propsMethodSpies = { 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 @@ -23,7 +25,7 @@ describe('AdvancedTabContent Component', function () { updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit} customGasPrice={11} customGasLimit={23456} - millisecondsRemaining={21500} + timeRemaining={21500} totalFee={'$0.25'} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) }) @@ -31,6 +33,10 @@ describe('AdvancedTabContent Component', function () { 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()', () => { @@ -40,12 +46,31 @@ describe('AdvancedTabContent Component', function () { it('should render the expected four children of the advanced-tab div', () => { const advancedTabChildren = wrapper.children() - assert.equal(advancedTabChildren.length, 4) + assert.equal(advancedTabChildren.length, 2) assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) - assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart-title')) - assert(advancedTabChildren.at(2).hasClass('advanced-tab__fee-chart')) - assert(advancedTabChildren.at(3).hasClass('advanced-tab__gas-edit-rows')) + 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).is(GasPriceChart)) + }) + + 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]) + + assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) + const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args + assert.deepEqual(renderGasEditRowArgs, [ + 11, propsMethodSpies.updateCustomGasPrice, 23456, propsMethodSpies.updateCustomGasLimit, + ]) + }) + + it('should call renderGasEditRows with the expected params', () => { + }) }) @@ -71,8 +96,8 @@ describe('AdvancedTabContent Component', function () { 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).is(TimeRemaining)) - assert.equal(dataNode.children().at(1).props().milliseconds, 'mockMsRemaining') + assert(dataNode.children().at(1).hasClass('time-remaining')) + assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining') }) }) @@ -138,7 +163,7 @@ describe('AdvancedTabContent Component', function () { const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args assert.equal(renderGasEditRowSpyArgs.length, 2) assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ - 'gasPriceNoDenom', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', 9, true, + 'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', 9, true, ].map(String)) assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 'mockGasLimit', 0, @@ -186,19 +211,6 @@ describe('AdvancedTabContent Component', function () { assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) }) - it('should show GWEI if the showGWEI prop is truthy', () => { - const gasInputWithGWEI = shallow(wrapper.instance().gasInput( - 321, - value => value + 7, - 0, - 8, - true - )) - assert.equal(gasInputWithGWEI.children().length, 2) - assert(gasInputWithGWEI.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) - assert(gasInputWithGWEI.children().at(1).hasClass('advanced-tab__gas-edit-row__gwei-symbol')) - }) - it('should pass the correct value min and precision props to the input', () => { const inputProps = gasInput.find('input').props() assert.equal(inputProps.min, 0) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss index 01bb06268..e2115af7f 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss @@ -1,13 +1,17 @@ .time-remaining { + color: #313A5E; + font-size: 16px; + .minutes-num, .seconds-num { - font-size: 26px; + font-size: 16px; } .seconds-num { margin-left: 7px; + font-size: 16px; } .minutes-label, .seconds-label { - font-size: 14px; + font-size: 16px; } } \ No newline at end of file diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss index d5eb5d414..b7b6c0f94 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/index.scss @@ -3,6 +3,7 @@ flex-direction: column; align-items: center; margin-bottom: 22px; + height: 291px; &__title { margin-top: 19px; diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 41fe901fa..07e55a1f0 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -24,8 +24,9 @@ export default class GasModalPageContainer extends Component { newTotalEth: PropTypes.string, }), onSubmit: PropTypes.func, - customGasPriceInHex: PropTypes.string, - customGasLimitInHex: PropTypes.string, + customModalGasPriceInHex: PropTypes.string, + customModalGasLimitInHex: PropTypes.string, + cancelAndClose: PropTypes.func, } state = {} @@ -51,32 +52,34 @@ export default class GasModalPageContainer extends Component { updateCustomGasLimit={convertThenUpdateCustomGasLimit} customGasPrice={customGasPrice} customGasLimit={customGasLimit} - millisecondsRemaining={91000} + timeRemaining={'1 min 31 sec'} totalFee={newTotalFiat} /> ) } - renderInfoRow (className, totalLabelKey, fiatTotal, cryptoTotal) { - return ( -
-
- {`${this.context.t(totalLabelKey)}:`} - {fiatTotal} -
-
- {`${this.context.t('amountPlusTxFee')}`} - {cryptoTotal} -
-
- ) - } + renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) { + const baseClassName = 'gas-modal-content__info-row' - renderInfoRows (originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) { return (
- { this.renderInfoRow('gas-modal-content__info-row--fade', 'originalTotal', originalTotalFiat, originalTotalEth) } - { this.renderInfoRow('gas-modal-content__info-row', 'newTotal', newTotalFiat, newTotalEth) } +
+
+ {`Send Amount`} + {sendAmount} +
+
+ {`Transaction Fee`} + {transactionFee} +
+
+ {`New Total`} + {newTotalEth} +
+
+ {newTotalFiat} +
+
) } @@ -86,6 +89,8 @@ export default class GasModalPageContainer extends Component { originalTotalEth, newTotalFiat, newTotalEth, + sendAmount, + transactionFee, }, { gasPriceButtonGroupProps, @@ -106,7 +111,7 @@ export default class GasModalPageContainer extends Component { {tabsToRender.map(({ name, content }, i) =>
{ content } - { this.renderInfoRows(originalTotalFiat, originalTotalEth, newTotalFiat, newTotalEth) } + { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
)} @@ -116,11 +121,11 @@ export default class GasModalPageContainer extends Component { render () { const { - hideModal, + cancelAndClose, infoRowProps, onSubmit, - customGasPriceInHex, - customGasLimitInHex, + customModalGasPriceInHex, + customModalGasLimitInHex, ...tabProps } = this.props @@ -131,13 +136,15 @@ export default class GasModalPageContainer extends Component { subtitle={this.context.t('customGasSubTitle')} tabsComponent={this.renderTabs(infoRowProps, tabProps)} disabled={false} - onCancel={() => hideModal()} - onClose={() => hideModal()} + onCancel={() => cancelAndClose()} + onClose={() => cancelAndClose()} onSubmit={() => { - onSubmit(customGasLimitInHex, customGasPriceInHex) - hideModal() + onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) + cancelAndClose() }} submitText={this.context.t('save')} + headerCloseText={'Close'} + hideCancel={true} />
) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index f9ed1cebb..ae233578b 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -9,6 +9,7 @@ import { import { setCustomGasPrice, setCustomGasLimit, + resetCustomData, } from '../../../ducks/gas.duck' import { hideGasButtonGroup, @@ -48,12 +49,12 @@ import { addHexPrefix } from 'ethereumjs-util' const mapStateToProps = state => { const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) - const { gasPrice, gas: gasLimit, value } = getTxParams(state) - const gasTotal = calcGasTotal(gasLimit, gasPrice) + const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state) + const gasTotal = calcGasTotal(currentGasLimit, currentGasPrice) - const customGasPriceInHex = getCustomGasPrice(state) - const customGasLimitInHex = getCustomGasLimit(state) - const customGasTotal = calcGasTotal(customGasLimitInHex || gasLimit, customGasPriceInHex || gasPrice) + const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice + const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit + const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) const gasButtonInfo = getRenderableBasicEstimateData(state) @@ -67,14 +68,14 @@ const mapStateToProps = state => { return { hideBasic, isConfirm: isConfirm(state), - customGasPriceInHex, - customGasLimitInHex, - customGasPrice: calcCustomGasPrice(customGasPriceInHex, gasPrice), - customGasLimit: calcCustomGasLimit(customGasLimitInHex, gasLimit), + customModalGasPriceInHex, + customModalGasLimitInHex, + customGasPrice: calcCustomGasPrice(customModalGasPriceInHex), + customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), newTotalFiat, gasPriceButtonGroupProps: { buttonDataLoading, - defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customGasPriceInHex, gasPrice), + defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), gasButtonInfo, }, infoRowProps: { @@ -82,6 +83,8 @@ const mapStateToProps = state => { originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), newTotalFiat, newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), + transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), + sendAmount: addHexWEIsToRenderableEth(value, '0x0'), }, } } @@ -90,7 +93,10 @@ const mapDispatchToProps = dispatch => { const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice))) return { - hideModal: () => dispatch(hideModal()), + cancelAndClose: () => { + dispatch(resetCustomData()) + dispatch(hideModal()) + }, updateCustomGasPrice, convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), @@ -138,12 +144,12 @@ function isConfirm (state) { return Boolean(Object.keys(state.confirmTransaction.txData).length) } -function calcCustomGasPrice (customGasPriceInHex, gasPrice) { - return Number(hexWEIToDecGWEI(customGasPriceInHex || gasPrice)) +function calcCustomGasPrice (customGasPriceInHex) { + return Number(hexWEIToDecGWEI(customGasPriceInHex)) } -function calcCustomGasLimit (customGasLimitInHex, gasLimit) { - return parseInt(customGasLimitInHex || gasLimit, 16) +function calcCustomGasLimit (customGasLimitInHex) { + return parseInt(customGasLimitInHex, 16) } function getTxParams (state) { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index f02045fe3..c49d69bf7 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -4,6 +4,57 @@ .gas-modal-page-container { .page-container { width: 391px; + + &__header { + padding: 0px; + padding-top: 16px; + + &--no-padding-bottom { + padding-bottom: 0; + } + } + + &__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; + } + + &__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; + } + } } } @@ -20,30 +71,35 @@ display: flex; flex-flow: column; color: $scorpion; + font-size: 12px; - &__total-info, &__sum-info { + &__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 { - &__total-label { + &__label { font-size: 16px; } - &__total-value { + &__value { font-size: 16px; font-weight: bold; } } - &__sum-info { - &__sum-label { + &__transaction-info, &__send-info { + &__label { font-size: 12px; } - &__sum-value { + &__value { font-size: 14px; } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index fdd7709d9..714f01538 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -9,7 +9,7 @@ import PageContainer from '../../../page-container' import { Tab } from '../../../tabs' const propsMethodSpies = { - hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), onSubmit: sinon.spy(), } @@ -39,12 +39,16 @@ const mockGasPriceButtonGroupProps = { 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 @@ -53,7 +57,7 @@ describe('GasModalPageContainer Component', function () { beforeEach(() => { wrapper = shallow( 'mockupdateCustomGasPrice'} updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} @@ -67,7 +71,7 @@ describe('GasModalPageContainer Component', function () { }) afterEach(() => { - propsMethodSpies.hideModal.resetHistory() + propsMethodSpies.cancelAndClose.resetHistory() }) describe('render', () => { @@ -91,11 +95,11 @@ describe('GasModalPageContainer Component', function () { onCancel, onClose, } = wrapper.find(PageContainer).props() - assert.equal(propsMethodSpies.hideModal.callCount, 0) + assert.equal(propsMethodSpies.cancelAndClose.callCount, 0) onCancel() - assert.equal(propsMethodSpies.hideModal.callCount, 1) + assert.equal(propsMethodSpies.cancelAndClose.callCount, 1) onClose() - assert.equal(propsMethodSpies.hideModal.callCount, 2) + assert.equal(propsMethodSpies.cancelAndClose.callCount, 2) }) it('should pass the correct renderTabs property to PageContainer', () => { @@ -158,8 +162,8 @@ describe('GasModalPageContainer Component', function () { assert.equal(GP.renderInfoRows.callCount, 2) - assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) - assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockOriginalTotalFiat', 'mockOriginalTotalEth', 'mockNewTotalFiat', 'mockNewTotalEth']) + 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', () => { @@ -176,25 +180,6 @@ describe('GasModalPageContainer Component', function () { }) }) - describe('renderInfoRow', () => { - it('should render a div with the passed className and two children, each with the expected text', () => { - const renderInfoRowResult = wrapper.instance().renderInfoRow('mockClassName', 'mockLabelKey', 'mockFiatAmount', 'mockCryptoAmount') - const renderedInfoRow = shallow(renderInfoRowResult) - assert.equal(renderedInfoRow.props().className, 'mockClassName') - - const firstChild = renderedInfoRow.childAt(0) - const secondhild = renderedInfoRow.childAt(1) - - assert.equal(firstChild.props().className, 'mockClassName__total-info') - assert.equal(secondhild.props().className, 'mockClassName__sum-info') - - assert.equal(firstChild.childAt(0).text(), 'mockLabelKey:') - assert.equal(firstChild.childAt(1).text(), 'mockFiatAmount') - assert.equal(secondhild.childAt(0).text(), 'amountPlusTxFee') - assert.equal(secondhild.childAt(1).text(), 'mockCryptoAmount') - }) - }) - describe('renderBasicTabContent', () => { it('should render', () => { const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps) @@ -220,8 +205,34 @@ describe('GasModalPageContainer Component', function () { assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit') assert.equal(advancedTabContentProps.customGasPrice, 123) assert.equal(advancedTabContentProps.customGasLimit, 456) - assert.equal(advancedTabContentProps.millisecondsRemaining, 91000) + assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec') assert.equal(advancedTabContentProps.totalFee, '$0.30') }) }) + + 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(), 'Send Amount mockSendAmount') + assert.equal(renderedInfoRows.at(1).text(), 'Transaction Fee mockTransactionFee') + assert.equal(renderedInfoRows.at(2).text(), 'New Total mockNewTotalEth') + assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat') + }) + }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 238f27ed6..3d9fb2624 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -15,6 +15,7 @@ const actionSpies = { const gasActionSpies = { setCustomGasPrice: sinon.spy(), setCustomGasLimit: sinon.spy(), + resetCustomData: sinon.spy(), } const confirmTransactionActionSpies = { @@ -37,7 +38,7 @@ proxyquire('../gas-modal-page-container.container.js', { '../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`, - getDefaultActiveButtonIndex: (a, b, c) => a + b + c, + getDefaultActiveButtonIndex: (a, b) => a + b, }, '../../../actions': actionSpies, '../../../ducks/gas.duck': gasActionSpies, @@ -89,15 +90,14 @@ describe('gas-modal-page-container container', () => { assert.deepEqual(result2, { isConfirm: true, - customGasPriceInHex: 'ffffffff', - customGasLimitInHex: 'aaaaaaaa', customGasPrice: 4.294967295, customGasLimit: 2863311530, newTotalFiat: '637.41', - gasPriceButtonGroupProps: - { + customModalGasLimitInHex: 'aaaaaaaa', + customModalGasPriceInHex: 'ffffffff', + gasPriceButtonGroupProps: { buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', - defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff0x3200000', + defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff', gasButtonInfo: 'mockRenderableBasicEstimateData:4', }, hideBasic: true, @@ -106,6 +106,8 @@ describe('gas-modal-page-container container', () => { originalTotalEth: '0.451569 ETH', newTotalFiat: '637.41', newTotalEth: '12.748189 ETH', + sendAmount: '0.45036 ETH', + transactionFee: '12.297829 ETH', }, }) }) @@ -135,11 +137,12 @@ describe('gas-modal-page-container container', () => { }) }) - describe('hideModal()', () => { + describe('cancelAndClose()', () => { it('should dispatch a hideModal action', () => { - mapDispatchToPropsObject.hideModal() - assert(dispatchSpy.calledOnce) + mapDispatchToPropsObject.cancelAndClose() + assert(dispatchSpy.calledTwice) assert(actionSpies.hideModal.calledOnce) + assert(gasActionSpies.resetCustomData.calledOnce) }) }) diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js new file mode 100644 index 000000000..7c32058b7 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class GasPriceChart extends Component { + static contextTypes = { + t: PropTypes.func, + } + + render () { + return ( +
+
+
+ ) + } +} diff --git a/ui/app/components/gas-customization/gas-price-chart/index.js b/ui/app/components/gas-customization/gas-price-chart/index.js new file mode 100644 index 000000000..9895acb62 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-chart/index.js @@ -0,0 +1 @@ +export { default } from './gas-price-chart.component' diff --git a/ui/app/components/gas-customization/gas-price-chart/index.scss b/ui/app/components/gas-customization/gas-price-chart/index.scss new file mode 100644 index 000000000..14e7a12a8 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-chart/index.scss @@ -0,0 +1,6 @@ +.gas-price-chart { + &__container { + display: flex; + position: relative; + } +} diff --git a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js new file mode 100644 index 000000000..474b7b7b6 --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../lib/shallow-with-context' +import GasPriceChart from '../gas-price-chart.component.js' + +describe('GasPriceChart Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render()', () => { + it('should render', () => { + console.log('wrapper', wrapper.html()) + assert(wrapper.hasClass('gas-price-chart')) + }) + + it('should render the chart div', () => { + assert(wrapper.childAt(0).hasClass('gas-price-chart__container')) + assert.equal(wrapper.childAt(0).props().id, 'chart') + }) + }) + +}) diff --git a/ui/app/components/gas-customization/index.scss b/ui/app/components/gas-customization/index.scss index 91ca5004e..e99d4e57f 100644 --- a/ui/app/components/gas-customization/index.scss +++ b/ui/app/components/gas-customization/index.scss @@ -1,3 +1,5 @@ @import './gas-slider/index'; @import './gas-modal-page-container/index'; + +@import './gas-price-chart/index'; diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js index 773fe1f56..5725c22ac 100644 --- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js @@ -12,6 +12,7 @@ export default class PageContainerFooter extends Component { submitText: PropTypes.string, disabled: PropTypes.bool, submitButtonType: PropTypes.string, + hideCancel: PropTypes.func, } static contextTypes = { @@ -27,20 +28,21 @@ export default class PageContainerFooter extends Component { submitText, disabled, submitButtonType, + hideCancel, } = this.props return (
- + } diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index c7ac175d9..cb4180508 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -8,9 +8,10 @@ import sinon from 'sinon' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), + onReset: sinon.spy(), } -describe('SendGasRow Component', function () { +describe('GasFeeDisplay Component', function () { let wrapper beforeEach(() => { @@ -20,6 +21,7 @@ describe('SendGasRow Component', function () { primaryCurrency={'mockPrimaryCurrency'} convertedCurrency={'mockConvertedCurrency'} showGasButtonGroup={propsMethodSpies.showCustomizeGasModal} + onReset={propsMethodSpies.onReset} />, {context: {t: str => str + '_t'}}) }) @@ -47,9 +49,9 @@ describe('SendGasRow Component', function () { className, } = wrapper.find('button').props() assert.equal(className, 'gas-fee-reset') - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + assert.equal(propsMethodSpies.onReset.callCount, 0) onClick() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + assert.equal(propsMethodSpies.onReset.callCount, 1) }) it('should render the reset button with the correct text', () => { diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js index 507407306..8d305dd4f 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js @@ -14,8 +14,8 @@ export default class SendGasRow extends Component { gasTotal: PropTypes.string, showCustomizeGasModal: PropTypes.func, gasPriceButtonGroupProps: PropTypes.object, - showGasButtonGroup: PropTypes.func, gasButtonGroupShown: PropTypes.bool, + resetGasButtons: PropTypes.func, } static contextTypes = { @@ -32,7 +32,7 @@ export default class SendGasRow extends Component { showCustomizeGasModal, gasPriceButtonGroupProps, gasButtonGroupShown, - showGasButtonGroup, + resetGasButtons, } = this.props return ( @@ -57,7 +57,7 @@ export default class SendGasRow extends Component { convertedCurrency={convertedCurrency} gasLoadingError={gasLoadingError} gasTotal={gasTotal} - showGasButtonGroup={showGasButtonGroup} + onReset={resetGasButtons} onClick={() => showCustomizeGasModal()} />} diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js index dd16559d0..39266e590 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js @@ -13,6 +13,9 @@ import { import { showGasButtonGroup, } from '../../../../ducks/send.duck' +import { + resetCustomData, +} from '../../../../ducks/gas.duck' import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js' import { showModal, setGasPrice } from '../../../../actions' import SendGasRow from './send-gas-row.component' @@ -44,13 +47,17 @@ function mapDispatchToProps (dispatch) { showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })), setGasPrice: newPrice => dispatch(setGasPrice(newPrice)), showGasButtonGroup: () => dispatch(showGasButtonGroup()), + resetCustomData: () => dispatch(resetCustomData()), } } function mergeProps (stateProps, dispatchProps, ownProps) { const { gasPriceButtonGroupProps } = stateProps + const { gasButtonInfo } = gasPriceButtonGroupProps const { setGasPrice: dispatchSetGasPrice, + showGasButtonGroup: dispatchShowGasButtonGroup, + resetCustomData: dispatchResetCustomData, ...otherDispatchProps } = dispatchProps @@ -62,5 +69,10 @@ function mergeProps (stateProps, dispatchProps, ownProps) { ...gasPriceButtonGroupProps, handleGasPriceSelection: dispatchSetGasPrice, }, + resetGasButtons: () => { + dispatchResetCustomData() + dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei) + dispatchShowGasButtonGroup() + }, } } diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js index 171cd7bf6..059c6cdd3 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -10,6 +10,7 @@ import GasPriceButtonGroup from '../../../../gas-customization/gas-price-button- const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), + resetGasButtons: sinon.spy(), } describe('SendGasRow Component', function () { @@ -22,9 +23,9 @@ describe('SendGasRow Component', function () { gasFeeError={'mockGasFeeError'} gasLoadingError={false} gasTotal={'mockGasTotal'} - showGasButtonGroup={'mockShowGasPriceButtonGroup'} gasButtonGroupShown={false} showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal} + resetGasButtons={propsMethodSpies.resetGasButtons} gasPriceButtonGroupProps={{ someGasPriceButtonGroupProp: 'foo', anotherGasPriceButtonGroupProp: 'bar', @@ -33,7 +34,7 @@ describe('SendGasRow Component', function () { }) afterEach(() => { - propsMethodSpies.showCustomizeGasModal.resetHistory() + propsMethodSpies.resetGasButtons.resetHistory() }) describe('render', () => { @@ -63,17 +64,15 @@ describe('SendGasRow Component', function () { convertedCurrency, gasLoadingError, gasTotal, - onClick, - showGasButtonGroup, + onReset, } = wrapper.find(SendRowWrapper).childAt(0).props() assert.equal(conversionRate, 20) assert.equal(convertedCurrency, 'mockConvertedCurrency') assert.equal(gasLoadingError, false) assert.equal(gasTotal, 'mockGasTotal') - assert.equal(showGasButtonGroup, 'mockShowGasPriceButtonGroup') - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) - onClick() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + assert.equal(propsMethodSpies.resetGasButtons.callCount, 0) + onReset() + assert.equal(propsMethodSpies.resetGasButtons.callCount, 1) }) it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => { diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js index 1c385e216..766bf6cab 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -15,6 +15,10 @@ const sendDuckSpies = { showGasButtonGroup: sinon.spy(), } +const gasDuckSpies = { + resetCustomData: sinon.spy(), +} + proxyquire('../send-gas-row.container.js', { 'react-redux': { connect: (ms, md, mp) => { @@ -42,6 +46,7 @@ proxyquire('../send-gas-row.container.js', { getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length, }, '../../../../ducks/send.duck': sendDuckSpies, + '../../../../ducks/gas.duck': gasDuckSpies, }) describe('send-gas-row container', () => { @@ -104,6 +109,14 @@ describe('send-gas-row container', () => { }) }) + describe('resetCustomData()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.resetCustomData() + assert(dispatchSpy.calledOnce) + assert(gasDuckSpies.resetCustomData.calledOnce) + }) + }) + }) describe('mergeProps', () => { diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index 96c00383b..009758cde 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -1,7 +1,16 @@ import assert from 'assert' import sinon from 'sinon' +import proxyquire from 'proxyquire' -import GasReducer, { + +const GasDuck = proxyquire('../gas.duck.js', { + '../../lib/local-storage-helpers': { + loadLocalStorageData: sinon.spy(), + saveLocalStorageData: sinon.spy(), + }, +}) + +const { basicGasEstimatesLoadingStarted, basicGasEstimatesLoadingFinished, setBasicGasEstimateData, @@ -16,7 +25,8 @@ import GasReducer, { setPricesAndTimeEstimates, fetchGasEstimates, setApiEstimatesLastRetrieved, -} from '../gas.duck.js' +} = GasDuck +const GasReducer = GasDuck.default describe('Gas Duck', () => { let tempFetch @@ -312,6 +322,11 @@ describe('Gas Duck', () => { 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( @@ -377,6 +392,46 @@ describe('Gas Duck', () => { [{ 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', () => { From 8c8359ca14efdbff2dd879f81090647036799607 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 6 Nov 2018 14:48:52 -0330 Subject: [PATCH 35/46] Remove gas customization integration tests (in favour of e2e tests) --- development/states/send-new-ui.json | 315 ++++++++++++++++++++++++++++ test/integration/lib/send-new-ui.js | 35 ---- ui/app/app.js | 3 +- 3 files changed, 317 insertions(+), 36 deletions(-) diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index 0cd2f23f2..9be4ef0da 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -159,4 +159,319 @@ "nonce": "", "fetchingMethodData": false } + "gas": { + "customData": { + "price": null, + "limit": "0x186a0" + }, + "basicEstimates": { + "average": 73, + "avgWait": 10.6, + "blockTime": 13.871657754010695, + "blockNum": 6655504, + "fast": 160, + "fastest": 290, + "fastestWait": 0.5, + "fastWait": 0.6, + "safeLow": 50, + "safeLowWait": 16.1, + "speed": 0.6702462692280712 + }, + "basicEstimateIsLoading": false, + "gasEstimatesLoading": false, + "priceAndTimeEstimates": [ + { + "expectedTime": "1374.1168296452973076627", + "expectedWait": 99.0593088449, + "gasprice": 4.1 + }, + { + "expectedTime": "1280.88976972896682763716", + "expectedWait": 92.3386225672, + "gasprice": 4.4 + }, + { + "expectedTime": "1245.13314632680319175597", + "expectedWait": 89.7609477113, + "gasprice": 4.8 + }, + { + "expectedTime": "1227.99925007911014385881", + "expectedWait": 88.5257747744, + "gasprice": 4.9 + }, + { + "expectedTime": "965.52572720362993349654", + "expectedWait": 69.6042062402, + "gasprice": 5 + }, + { + "expectedTime": "917.466895447437420776", + "expectedWait": 66.1396721082, + "gasprice": 5.1 + }, + { + "expectedTime": "915.81694044041496090521", + "expectedWait": 66.0207277804, + "gasprice": 5.2 + }, + { + "expectedTime": "902.13145619709089691874", + "expectedWait": 65.034148924, + "gasprice": 5.3 + }, + { + "expectedTime": "890.83591122200105749896", + "expectedWait": 64.2198594443, + "gasprice": 5.4 + }, + { + "expectedTime": "879.10469542971335712248", + "expectedWait": 63.3741627006, + "gasprice": 5.5 + }, + { + "expectedTime": "876.99737395823100420974", + "expectedWait": 63.2222470818, + "gasprice": 5.6 + }, + { + "expectedTime": "865.96781957003849098957", + "expectedWait": 62.4271327138, + "gasprice": 5.7 + }, + { + "expectedTime": "865.44839472121496158482", + "expectedWait": 62.3896876688, + "gasprice": 5.8 + }, + { + "expectedTime": "802.16173170976255602161", + "expectedWait": 57.8273877524, + "gasprice": 6 + }, + { + "expectedTime": "780.79313908053047074843", + "expectedWait": 56.2869379368, + "gasprice": 6.1 + }, + { + "expectedTime": "770.04888359616469549233", + "expectedWait": 55.5123906062, + "gasprice": 6.2 + }, + { + "expectedTime": "745.01007965146736962697", + "expectedWait": 53.7073573226, + "gasprice": 6.3 + }, + { + "expectedTime": "735.19921111598501681816", + "expectedWait": 53.0000973318, + "gasprice": 6.6 + }, + { + "expectedTime": "705.68767153912619368694", + "expectedWait": 50.8726270539, + "gasprice": 6.7 + }, + { + "expectedTime": "705.26438593445239690121", + "expectedWait": 50.8421126329, + "gasprice": 6.9 + }, + { + "expectedTime": "652.51573119854865429742", + "expectedWait": 47.0394918019, + "gasprice": 7 + }, + { + "expectedTime": "635.51471669299464383162", + "expectedWait": 45.813898235, + "gasprice": 7.1 + }, + { + "expectedTime": "634.37181911960854759036", + "expectedWait": 45.7315073922, + "gasprice": 7.2 + }, + { + "expectedTime": "633.23097691113902888918", + "expectedWait": 45.6492647195, + "gasprice": 7.3 + }, + { + "expectedTime": "112.7753456245379663928", + "expectedWait": 8.1299111919, + "gasprice": 7.6 + }, + { + "expectedTime": "102.9665314468898381829", + "expectedWait": 7.4227992986, + "gasprice": 8 + }, + { + "expectedTime": "100.94784507024919649891", + "expectedWait": 7.2772733339, + "gasprice": 8.1 + }, + { + "expectedTime": "100.46445647447807351078", + "expectedWait": 7.2424261221, + "gasprice": 8.8 + }, + { + "expectedTime": "84.91686745986737853339", + "expectedWait": 6.1216091808, + "gasprice": 9 + }, + { + "expectedTime": "80.39566429296684383503", + "expectedWait": 5.7956781892, + "gasprice": 9.1 + }, + { + "expectedTime": "78.24522052614759252715", + "expectedWait": 5.6406539084, + "gasprice": 9.2 + }, + { + "expectedTime": "77.1685119880459882636", + "expectedWait": 5.5630345959, + "gasprice": 9.5 + }, + { + "expectedTime": "72.43649507646737870178", + "expectedWait": 5.2219061601, + "gasprice": 9.8 + }, + { + "expectedTime": "71.48259532351443753818", + "expectedWait": 5.1531400638, + "gasprice": 9.9 + }, + { + "expectedTime": "58.23892805162994573827", + "expectedWait": 4.1984115442, + "gasprice": 10 + }, + { + "expectedTime": "53.13065124862245917617", + "expectedWait": 3.8301587446, + "gasprice": 10.1 + }, + { + "expectedTime": "53.03510209647058751971", + "expectedWait": 3.82327066, + "gasprice": 10.3 + }, + { + "expectedTime": "49.06846157804491912403", + "expectedWait": 3.5373177776, + "gasprice": 11 + }, + { + "expectedTime": "48.30893330101818116637", + "expectedWait": 3.4825638116, + "gasprice": 11.1 + }, + { + "expectedTime": "48.25099734861818116715", + "expectedWait": 3.4783872414, + "gasprice": 11.3 + }, + { + "expectedTime": "47.64416885027272662988", + "expectedWait": 3.4346413165, + "gasprice": 11.9 + }, + { + "expectedTime": "46.76354741392085498401", + "expectedWait": 3.3711578128, + "gasprice": 12.6 + }, + { + "expectedTime": "44.99427448545882292232", + "expectedWait": 3.2436119232, + "gasprice": 13 + }, + { + "expectedTime": "44.61790554199251276697", + "expectedWait": 3.2164796979, + "gasprice": 13.1 + }, + { + "expectedTime": "42.87832690973048070488", + "expectedWait": 3.0910744534, + "gasprice": 14 + }, + { + "expectedTime": "42.21224091308663044649", + "expectedWait": 3.0430566888, + "gasprice": 14.9 + }, + { + "expectedTime": "41.15715335111336842864", + "expectedWait": 2.9669960203, + "gasprice": 15 + }, + { + "expectedTime": "40.9600723880876999821", + "expectedWait": 2.9527885646, + "gasprice": 15.1 + }, + { + "expectedTime": "38.89138450301711177472", + "expectedWait": 2.8036580193, + "gasprice": 15.8 + }, + { + "expectedTime": "37.89655640860213852611", + "expectedWait": 2.7319414219, + "gasprice": 16 + }, + { + "expectedTime": "37.35265517364705831954", + "expectedWait": 2.692731888, + "gasprice": 17.4 + }, + { + "expectedTime": "36.79447683873796741798", + "expectedWait": 2.652493126, + "gasprice": 17.8 + }, + { + "expectedTime": "36.11439350850802090309", + "expectedWait": 2.6034663015, + "gasprice": 19 + }, + { + "expectedTime": "31.32676199432192471101", + "expectedWait": 2.2583286403, + "gasprice": 20 + }, + { + "expectedTime": "30.76792490132192471855", + "expectedWait": 2.2180423888, + "gasprice": 20.1 + }, + { + "expectedTime": "29.94493658520962526441", + "expectedWait": 2.1587136243, + "gasprice": 25 + }, + { + "expectedTime": "29.53287347625561457478", + "expectedWait": 2.1290082267, + "gasprice": 29 + }, + { + "expectedTime": "29.09318627175614934008", + "expectedWait": 2.0973114236, + "gasprice": 47 + } + ], + "priceAndTimeEstimatesLastRetrieved": 1541527901281, + "errors": {} + }, } diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 271dd91cf..98e5f549b 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -21,37 +21,6 @@ global.ethQuery = { global.ethereumProvider = {} -async function customizeGas (assert, price, limit, ethFee, usdFee) { - const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container') - sendGasOpenCustomizeModalButton[0].click() - - const customizeGasModal = await queryAsync($, '.send-v2__customize-gas') - assert.ok(customizeGasModal[0], 'should render the customize gas modal') - - const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input') - customizeGasPriceInput.val(price) - reactTriggerChange(customizeGasPriceInput[0]) - const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input') - customizeGasLimitInput.val(limit) - reactTriggerChange(customizeGasLimitInput[0]) - - const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save') - customizeGasSaveButton[0].click() - const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') - - assert.equal( - (await findAsync(sendGasField, '.currency-display-component'))[0].textContent, - ethFee, - 'send gas field should show customized gas total' - ) - - assert.equal( - (await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent, - usdFee, - 'send gas field should show customized gas total converted to USD' - ) -} - async function runSendFlowTest (assert, done) { console.log('*** start runSendFlowTest') const selectState = await queryAsync($, 'select') @@ -112,10 +81,6 @@ async function runSendFlowTest (assert, done) { errorMessage = $('.send-v2__error') assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected') - await customizeGas(assert, 0, 21000, '0ETH', '$0.00USD') - await customizeGas(assert, 1, 21000, '0.000021ETH', '$0.03USD') - await customizeGas(assert, 500, 60000, '0.03ETH', '$36.03USD') - const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') sendButton[0].click() diff --git a/ui/app/app.js b/ui/app/app.js index 10428d1fd..7669a5db9 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -121,8 +121,9 @@ class App extends Component { isOpen: sidebarIsOpen, transitionName: sidebarTransitionName, type: sidebarType, - props: { transaction: sidebarTransaction }, + props, } = sidebar + const { transaction: sidebarTransaction } = props || {} return ( h('.flex-column.full-height', { From b7f873b96ecee8d4b6b3d78a3cde7b2495967970 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 7 Nov 2018 10:02:17 -0330 Subject: [PATCH 36/46] Add gas data to integration test json data set. --- development/states/send-edit.json | 315 +++++++++++++++++++++++++++ development/states/send-new-ui.json | 4 +- development/states/send.json | 317 +++++++++++++++++++++++++++- 3 files changed, 633 insertions(+), 3 deletions(-) diff --git a/development/states/send-edit.json b/development/states/send-edit.json index 6330b777d..a519f30b4 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -176,5 +176,320 @@ "hexGasTotal": "", "nonce": "", "fetchingMethodData": false + }, + "gas": { + "customData": { + "price": null, + "limit": "0x186a0" + }, + "basicEstimates": { + "average": 73, + "avgWait": 10.6, + "blockTime": 13.871657754010695, + "blockNum": 6655504, + "fast": 160, + "fastest": 290, + "fastestWait": 0.5, + "fastWait": 0.6, + "safeLow": 50, + "safeLowWait": 16.1, + "speed": 0.6702462692280712 + }, + "basicEstimateIsLoading": false, + "gasEstimatesLoading": false, + "priceAndTimeEstimates": [ + { + "expectedTime": "1374.1168296452973076627", + "expectedWait": 99.0593088449, + "gasprice": 4.1 + }, + { + "expectedTime": "1280.88976972896682763716", + "expectedWait": 92.3386225672, + "gasprice": 4.4 + }, + { + "expectedTime": "1245.13314632680319175597", + "expectedWait": 89.7609477113, + "gasprice": 4.8 + }, + { + "expectedTime": "1227.99925007911014385881", + "expectedWait": 88.5257747744, + "gasprice": 4.9 + }, + { + "expectedTime": "965.52572720362993349654", + "expectedWait": 69.6042062402, + "gasprice": 5 + }, + { + "expectedTime": "917.466895447437420776", + "expectedWait": 66.1396721082, + "gasprice": 5.1 + }, + { + "expectedTime": "915.81694044041496090521", + "expectedWait": 66.0207277804, + "gasprice": 5.2 + }, + { + "expectedTime": "902.13145619709089691874", + "expectedWait": 65.034148924, + "gasprice": 5.3 + }, + { + "expectedTime": "890.83591122200105749896", + "expectedWait": 64.2198594443, + "gasprice": 5.4 + }, + { + "expectedTime": "879.10469542971335712248", + "expectedWait": 63.3741627006, + "gasprice": 5.5 + }, + { + "expectedTime": "876.99737395823100420974", + "expectedWait": 63.2222470818, + "gasprice": 5.6 + }, + { + "expectedTime": "865.96781957003849098957", + "expectedWait": 62.4271327138, + "gasprice": 5.7 + }, + { + "expectedTime": "865.44839472121496158482", + "expectedWait": 62.3896876688, + "gasprice": 5.8 + }, + { + "expectedTime": "802.16173170976255602161", + "expectedWait": 57.8273877524, + "gasprice": 6 + }, + { + "expectedTime": "780.79313908053047074843", + "expectedWait": 56.2869379368, + "gasprice": 6.1 + }, + { + "expectedTime": "770.04888359616469549233", + "expectedWait": 55.5123906062, + "gasprice": 6.2 + }, + { + "expectedTime": "745.01007965146736962697", + "expectedWait": 53.7073573226, + "gasprice": 6.3 + }, + { + "expectedTime": "735.19921111598501681816", + "expectedWait": 53.0000973318, + "gasprice": 6.6 + }, + { + "expectedTime": "705.68767153912619368694", + "expectedWait": 50.8726270539, + "gasprice": 6.7 + }, + { + "expectedTime": "705.26438593445239690121", + "expectedWait": 50.8421126329, + "gasprice": 6.9 + }, + { + "expectedTime": "652.51573119854865429742", + "expectedWait": 47.0394918019, + "gasprice": 7 + }, + { + "expectedTime": "635.51471669299464383162", + "expectedWait": 45.813898235, + "gasprice": 7.1 + }, + { + "expectedTime": "634.37181911960854759036", + "expectedWait": 45.7315073922, + "gasprice": 7.2 + }, + { + "expectedTime": "633.23097691113902888918", + "expectedWait": 45.6492647195, + "gasprice": 7.3 + }, + { + "expectedTime": "112.7753456245379663928", + "expectedWait": 8.1299111919, + "gasprice": 7.6 + }, + { + "expectedTime": "102.9665314468898381829", + "expectedWait": 7.4227992986, + "gasprice": 8 + }, + { + "expectedTime": "100.94784507024919649891", + "expectedWait": 7.2772733339, + "gasprice": 8.1 + }, + { + "expectedTime": "100.46445647447807351078", + "expectedWait": 7.2424261221, + "gasprice": 8.8 + }, + { + "expectedTime": "84.91686745986737853339", + "expectedWait": 6.1216091808, + "gasprice": 9 + }, + { + "expectedTime": "80.39566429296684383503", + "expectedWait": 5.7956781892, + "gasprice": 9.1 + }, + { + "expectedTime": "78.24522052614759252715", + "expectedWait": 5.6406539084, + "gasprice": 9.2 + }, + { + "expectedTime": "77.1685119880459882636", + "expectedWait": 5.5630345959, + "gasprice": 9.5 + }, + { + "expectedTime": "72.43649507646737870178", + "expectedWait": 5.2219061601, + "gasprice": 9.8 + }, + { + "expectedTime": "71.48259532351443753818", + "expectedWait": 5.1531400638, + "gasprice": 9.9 + }, + { + "expectedTime": "58.23892805162994573827", + "expectedWait": 4.1984115442, + "gasprice": 10 + }, + { + "expectedTime": "53.13065124862245917617", + "expectedWait": 3.8301587446, + "gasprice": 10.1 + }, + { + "expectedTime": "53.03510209647058751971", + "expectedWait": 3.82327066, + "gasprice": 10.3 + }, + { + "expectedTime": "49.06846157804491912403", + "expectedWait": 3.5373177776, + "gasprice": 11 + }, + { + "expectedTime": "48.30893330101818116637", + "expectedWait": 3.4825638116, + "gasprice": 11.1 + }, + { + "expectedTime": "48.25099734861818116715", + "expectedWait": 3.4783872414, + "gasprice": 11.3 + }, + { + "expectedTime": "47.64416885027272662988", + "expectedWait": 3.4346413165, + "gasprice": 11.9 + }, + { + "expectedTime": "46.76354741392085498401", + "expectedWait": 3.3711578128, + "gasprice": 12.6 + }, + { + "expectedTime": "44.99427448545882292232", + "expectedWait": 3.2436119232, + "gasprice": 13 + }, + { + "expectedTime": "44.61790554199251276697", + "expectedWait": 3.2164796979, + "gasprice": 13.1 + }, + { + "expectedTime": "42.87832690973048070488", + "expectedWait": 3.0910744534, + "gasprice": 14 + }, + { + "expectedTime": "42.21224091308663044649", + "expectedWait": 3.0430566888, + "gasprice": 14.9 + }, + { + "expectedTime": "41.15715335111336842864", + "expectedWait": 2.9669960203, + "gasprice": 15 + }, + { + "expectedTime": "40.9600723880876999821", + "expectedWait": 2.9527885646, + "gasprice": 15.1 + }, + { + "expectedTime": "38.89138450301711177472", + "expectedWait": 2.8036580193, + "gasprice": 15.8 + }, + { + "expectedTime": "37.89655640860213852611", + "expectedWait": 2.7319414219, + "gasprice": 16 + }, + { + "expectedTime": "37.35265517364705831954", + "expectedWait": 2.692731888, + "gasprice": 17.4 + }, + { + "expectedTime": "36.79447683873796741798", + "expectedWait": 2.652493126, + "gasprice": 17.8 + }, + { + "expectedTime": "36.11439350850802090309", + "expectedWait": 2.6034663015, + "gasprice": 19 + }, + { + "expectedTime": "31.32676199432192471101", + "expectedWait": 2.2583286403, + "gasprice": 20 + }, + { + "expectedTime": "30.76792490132192471855", + "expectedWait": 2.2180423888, + "gasprice": 20.1 + }, + { + "expectedTime": "29.94493658520962526441", + "expectedWait": 2.1587136243, + "gasprice": 25 + }, + { + "expectedTime": "29.53287347625561457478", + "expectedWait": 2.1290082267, + "gasprice": 29 + }, + { + "expectedTime": "29.09318627175614934008", + "expectedWait": 2.0973114236, + "gasprice": 47 + } + ], + "priceAndTimeEstimatesLastRetrieved": 1541527901281, + "errors": {} } } diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index 9be4ef0da..479b6d3e3 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -158,7 +158,7 @@ "hexGasTotal": "", "nonce": "", "fetchingMethodData": false - } + }, "gas": { "customData": { "price": null, @@ -473,5 +473,5 @@ ], "priceAndTimeEstimatesLastRetrieved": 1541527901281, "errors": {} - }, + } } diff --git a/development/states/send.json b/development/states/send.json index 4c67f8ac6..8ae385564 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -107,5 +107,320 @@ "scrollToBottom": false, "forgottenPassword": null }, - "identities": {} + "identities": {}, + "gas": { + "customData": { + "price": null, + "limit": "0x186a0" + }, + "basicEstimates": { + "average": 73, + "avgWait": 10.6, + "blockTime": 13.871657754010695, + "blockNum": 6655504, + "fast": 160, + "fastest": 290, + "fastestWait": 0.5, + "fastWait": 0.6, + "safeLow": 50, + "safeLowWait": 16.1, + "speed": 0.6702462692280712 + }, + "basicEstimateIsLoading": false, + "gasEstimatesLoading": false, + "priceAndTimeEstimates": [ + { + "expectedTime": "1374.1168296452973076627", + "expectedWait": 99.0593088449, + "gasprice": 4.1 + }, + { + "expectedTime": "1280.88976972896682763716", + "expectedWait": 92.3386225672, + "gasprice": 4.4 + }, + { + "expectedTime": "1245.13314632680319175597", + "expectedWait": 89.7609477113, + "gasprice": 4.8 + }, + { + "expectedTime": "1227.99925007911014385881", + "expectedWait": 88.5257747744, + "gasprice": 4.9 + }, + { + "expectedTime": "965.52572720362993349654", + "expectedWait": 69.6042062402, + "gasprice": 5 + }, + { + "expectedTime": "917.466895447437420776", + "expectedWait": 66.1396721082, + "gasprice": 5.1 + }, + { + "expectedTime": "915.81694044041496090521", + "expectedWait": 66.0207277804, + "gasprice": 5.2 + }, + { + "expectedTime": "902.13145619709089691874", + "expectedWait": 65.034148924, + "gasprice": 5.3 + }, + { + "expectedTime": "890.83591122200105749896", + "expectedWait": 64.2198594443, + "gasprice": 5.4 + }, + { + "expectedTime": "879.10469542971335712248", + "expectedWait": 63.3741627006, + "gasprice": 5.5 + }, + { + "expectedTime": "876.99737395823100420974", + "expectedWait": 63.2222470818, + "gasprice": 5.6 + }, + { + "expectedTime": "865.96781957003849098957", + "expectedWait": 62.4271327138, + "gasprice": 5.7 + }, + { + "expectedTime": "865.44839472121496158482", + "expectedWait": 62.3896876688, + "gasprice": 5.8 + }, + { + "expectedTime": "802.16173170976255602161", + "expectedWait": 57.8273877524, + "gasprice": 6 + }, + { + "expectedTime": "780.79313908053047074843", + "expectedWait": 56.2869379368, + "gasprice": 6.1 + }, + { + "expectedTime": "770.04888359616469549233", + "expectedWait": 55.5123906062, + "gasprice": 6.2 + }, + { + "expectedTime": "745.01007965146736962697", + "expectedWait": 53.7073573226, + "gasprice": 6.3 + }, + { + "expectedTime": "735.19921111598501681816", + "expectedWait": 53.0000973318, + "gasprice": 6.6 + }, + { + "expectedTime": "705.68767153912619368694", + "expectedWait": 50.8726270539, + "gasprice": 6.7 + }, + { + "expectedTime": "705.26438593445239690121", + "expectedWait": 50.8421126329, + "gasprice": 6.9 + }, + { + "expectedTime": "652.51573119854865429742", + "expectedWait": 47.0394918019, + "gasprice": 7 + }, + { + "expectedTime": "635.51471669299464383162", + "expectedWait": 45.813898235, + "gasprice": 7.1 + }, + { + "expectedTime": "634.37181911960854759036", + "expectedWait": 45.7315073922, + "gasprice": 7.2 + }, + { + "expectedTime": "633.23097691113902888918", + "expectedWait": 45.6492647195, + "gasprice": 7.3 + }, + { + "expectedTime": "112.7753456245379663928", + "expectedWait": 8.1299111919, + "gasprice": 7.6 + }, + { + "expectedTime": "102.9665314468898381829", + "expectedWait": 7.4227992986, + "gasprice": 8 + }, + { + "expectedTime": "100.94784507024919649891", + "expectedWait": 7.2772733339, + "gasprice": 8.1 + }, + { + "expectedTime": "100.46445647447807351078", + "expectedWait": 7.2424261221, + "gasprice": 8.8 + }, + { + "expectedTime": "84.91686745986737853339", + "expectedWait": 6.1216091808, + "gasprice": 9 + }, + { + "expectedTime": "80.39566429296684383503", + "expectedWait": 5.7956781892, + "gasprice": 9.1 + }, + { + "expectedTime": "78.24522052614759252715", + "expectedWait": 5.6406539084, + "gasprice": 9.2 + }, + { + "expectedTime": "77.1685119880459882636", + "expectedWait": 5.5630345959, + "gasprice": 9.5 + }, + { + "expectedTime": "72.43649507646737870178", + "expectedWait": 5.2219061601, + "gasprice": 9.8 + }, + { + "expectedTime": "71.48259532351443753818", + "expectedWait": 5.1531400638, + "gasprice": 9.9 + }, + { + "expectedTime": "58.23892805162994573827", + "expectedWait": 4.1984115442, + "gasprice": 10 + }, + { + "expectedTime": "53.13065124862245917617", + "expectedWait": 3.8301587446, + "gasprice": 10.1 + }, + { + "expectedTime": "53.03510209647058751971", + "expectedWait": 3.82327066, + "gasprice": 10.3 + }, + { + "expectedTime": "49.06846157804491912403", + "expectedWait": 3.5373177776, + "gasprice": 11 + }, + { + "expectedTime": "48.30893330101818116637", + "expectedWait": 3.4825638116, + "gasprice": 11.1 + }, + { + "expectedTime": "48.25099734861818116715", + "expectedWait": 3.4783872414, + "gasprice": 11.3 + }, + { + "expectedTime": "47.64416885027272662988", + "expectedWait": 3.4346413165, + "gasprice": 11.9 + }, + { + "expectedTime": "46.76354741392085498401", + "expectedWait": 3.3711578128, + "gasprice": 12.6 + }, + { + "expectedTime": "44.99427448545882292232", + "expectedWait": 3.2436119232, + "gasprice": 13 + }, + { + "expectedTime": "44.61790554199251276697", + "expectedWait": 3.2164796979, + "gasprice": 13.1 + }, + { + "expectedTime": "42.87832690973048070488", + "expectedWait": 3.0910744534, + "gasprice": 14 + }, + { + "expectedTime": "42.21224091308663044649", + "expectedWait": 3.0430566888, + "gasprice": 14.9 + }, + { + "expectedTime": "41.15715335111336842864", + "expectedWait": 2.9669960203, + "gasprice": 15 + }, + { + "expectedTime": "40.9600723880876999821", + "expectedWait": 2.9527885646, + "gasprice": 15.1 + }, + { + "expectedTime": "38.89138450301711177472", + "expectedWait": 2.8036580193, + "gasprice": 15.8 + }, + { + "expectedTime": "37.89655640860213852611", + "expectedWait": 2.7319414219, + "gasprice": 16 + }, + { + "expectedTime": "37.35265517364705831954", + "expectedWait": 2.692731888, + "gasprice": 17.4 + }, + { + "expectedTime": "36.79447683873796741798", + "expectedWait": 2.652493126, + "gasprice": 17.8 + }, + { + "expectedTime": "36.11439350850802090309", + "expectedWait": 2.6034663015, + "gasprice": 19 + }, + { + "expectedTime": "31.32676199432192471101", + "expectedWait": 2.2583286403, + "gasprice": 20 + }, + { + "expectedTime": "30.76792490132192471855", + "expectedWait": 2.2180423888, + "gasprice": 20.1 + }, + { + "expectedTime": "29.94493658520962526441", + "expectedWait": 2.1587136243, + "gasprice": 25 + }, + { + "expectedTime": "29.53287347625561457478", + "expectedWait": 2.1290082267, + "gasprice": 29 + }, + { + "expectedTime": "29.09318627175614934008", + "expectedWait": 2.0973114236, + "gasprice": 47 + } + ], + "priceAndTimeEstimatesLastRetrieved": 1541527901281, + "errors": {} + } } From a44596ec0dc45fa0d3a950847ff68f7a53f9625f Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 8 Nov 2018 07:53:34 -0330 Subject: [PATCH 37/46] Add c3 and d3 to the separate dependencies bundle. --- gulpfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 1487f717e..31888faac 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,10 +30,12 @@ const packageJSON = require('./package.json') const dependencies = Object.keys(packageJSON && packageJSON.dependencies || {}) const materialUIDependencies = ['@material-ui/core'] const reactDepenendencies = dependencies.filter(dep => dep.match(/react/)) +const d3Dependencies = ['c3', 'd3'] const uiDependenciesToBundle = [ ...materialUIDependencies, ...reactDepenendencies, + ...d3Dependencies, ] function gulpParallel (...args) { From d5411e772d1efd8d723eb81403fa22b03b904f49 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 8 Nov 2018 12:33:31 -0330 Subject: [PATCH 38/46] Make gas customization modal responsive. --- .../gas-customization/gas-modal-page-container/index.scss | 8 ++++++++ .../gas-customization/gas-price-button-group/index.scss | 8 ++++++++ .../gas-customization/gas-price-chart/index.scss | 5 +++++ ui/app/components/page-container/index.scss | 6 +++++- ui/app/css/itcss/components/newui-sections.scss | 1 + ui/app/css/itcss/components/send.scss | 1 + ui/app/css/itcss/generic/index.scss | 4 ++++ 7 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index debd9b5ee..9486eaf8f 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -6,6 +6,10 @@ max-width: 391px; min-height: 585px; + @media screen and (max-width: $break-small) { + max-width: 344px; + } + &__header { padding: 0px; padding-top: 16px; @@ -60,6 +64,10 @@ } .gas-modal-content { + @media screen and (max-width: $break-small) { + width: 100%; + } + &__basic-tab { height: 219px; } diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/gas-customization/gas-price-button-group/index.scss index 22661ed7b..6e9d01c6e 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/index.scss +++ b/ui/app/components/gas-customization/gas-price-button-group/index.scss @@ -67,6 +67,10 @@ justify-content: stretch; max-width: 260px; + @media screen and (max-width: $break-small) { + max-width: 230px; + } + &__button-fiat-price { font-size: 13px; } @@ -98,6 +102,10 @@ padding-top: 9px; padding-left: 8.5px; + @media screen and (max-width: $break-small) { + padding-left: 4px; + } + div { display: flex; flex-flow: column; diff --git a/ui/app/components/gas-customization/gas-price-chart/index.scss b/ui/app/components/gas-customization/gas-price-chart/index.scss index 0990d6abb..097543104 100644 --- a/ui/app/components/gas-customization/gas-price-chart/index.scss +++ b/ui/app/components/gas-customization/gas-price-chart/index.scss @@ -1,12 +1,17 @@ .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 { diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss index ba1215e84..1634ab012 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/page-container/index.scss @@ -194,10 +194,14 @@ .page-container { height: 100%; width: 100%; - overflow-y: auto; background-color: $white; border-radius: 0; flex: 1; + + &__content { + display: flex; + overflow-y: initial; + } } } diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 233e781ef..b4b77ba1e 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -216,6 +216,7 @@ $wallet-view-bg: $alabaster; .main-container-wrapper { height: 100%; + width: 100%; } } diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 4535ced52..ba7db7aa8 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -530,6 +530,7 @@ @media screen and (max-width: $break-small) { margin: 0; flex: 1 1 auto; + width: 100%; } } diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss index d1c65afed..d8e62c97a 100644 --- a/ui/app/css/itcss/generic/index.scss +++ b/ui/app/css/itcss/generic/index.scss @@ -18,6 +18,10 @@ body { height: 100%; margin: 0; padding: 0; + + @media screen and (max-width: $break-small) { + overflow-y: overlay; + } } html { From 4111e9f92d9b87fa46a9f28e767cdf6bfce21fa2 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 15 Nov 2018 17:02:12 -0330 Subject: [PATCH 39/46] Improve responsiveness of customize speed up slider. --- .../gas-modal-page-container/index.scss | 19 ++++++ ui/app/components/sidebars/index.scss | 5 ++ .../components/sidebars/sidebar-content.scss | 58 +++++++++++++++++-- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 9486eaf8f..2532c1fc2 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -19,6 +19,13 @@ } } + &__footer { + header { + padding-top: 12px; + padding-bottom: 12px; + } + } + &__header-close-text { font-size: 14px; color: #4EADE7; @@ -82,6 +89,10 @@ 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; @@ -95,11 +106,19 @@ &__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; + } } } diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/sidebars/index.scss index 6fcd93e99..b9845d564 100644 --- a/ui/app/components/sidebars/index.scss +++ b/ui/app/components/sidebars/index.scss @@ -60,6 +60,11 @@ width: 408px; left: calc(100% - 408px); } + + @media screen and (max-width: $break-small) { + width: 100%; + left: 0%; + } } .sidebar-overlay { diff --git a/ui/app/components/sidebars/sidebar-content.scss b/ui/app/components/sidebars/sidebar-content.scss index c0973be2c..7d1f719dc 100644 --- a/ui/app/components/sidebars/sidebar-content.scss +++ b/ui/app/components/sidebars/sidebar-content.scss @@ -2,10 +2,24 @@ .gas-modal-page-container { .page-container { max-width: 100%; + + &__content { + display: flex; + overflow-y: initial; + } + + @media screen and (max-width: $break-small) { + max-width: 344px; + min-height: auto; + } } .gas-price-chart { margin-left: 10px; + + &__root { + max-height: 160px !important; + } } .page-container__bottom { @@ -20,21 +34,53 @@ } .basic-tab-content { - height: 377px; + height: 318px; margin-bottom: 0px; border-bottom: 1px solid #d2d8dd; - } - .advanced-tab__fee-chart { - height: 320px; + @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__fee-chart__speed-buttons { - bottom: 77px; + .advanced-tab { + &__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__info-row { height: 170px; + + @media screen and (max-width: $break-small) { + height: initial; + display: flex; + justify-content: center; + } } } } \ No newline at end of file From fe535159bb3ec5849d670d9bc53067f5d6f330b7 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 12:26:12 -0330 Subject: [PATCH 40/46] Fix "fastest" translation message; change to sentence case --- app/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 69b9969be..d23de5fa0 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -449,7 +449,7 @@ "message": "Fast" }, "fastest": { - "message": "FASTEST" + "message": "Fastest" }, "feeChartTitle": { "message": "Live Transaction Fee Predictions" From 7f2c5c09de67a67972fcbaae254d39aac6c96f56 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 13:06:52 -0330 Subject: [PATCH 41/46] Uses more reliable api on main send screen; caches basic api results in modal --- test/e2e/beta/fetch-mocks.js | 1 + test/e2e/beta/metamask-beta-ui.spec.js | 4 +- .../gas-modal-page-container.component.js | 19 ++++ .../gas-modal-page-container.container.js | 10 +- ...gas-modal-page-container-component.test.js | 32 +++++- ...gas-modal-page-container-container.test.js | 4 + .../confirm-transaction.component.js | 6 +- .../confirm-transaction.container.js | 4 +- .../send-gas-row/send-gas-row.container.js | 4 +- .../tests/send-gas-row-container.test.js | 2 +- ui/app/components/send/send.component.js | 4 +- ui/app/components/send/send.container.js | 2 - ui/app/components/send/send.selectors.js | 4 +- .../send/tests/send-component.test.js | 18 +--- .../transaction-list-item.component.js | 6 +- .../transaction-list-item.container.js | 4 +- ui/app/ducks/gas.duck.js | 98 ++++++++++++++++--- ui/app/ducks/tests/gas-duck.test.js | 34 ++++++- ui/app/selectors/custom-gas.js | 94 +++++++++++------- ui/app/selectors/tests/custom-gas.test.js | 94 +++++++++--------- ui/lib/test-timeout.js | 5 + 21 files changed, 309 insertions(+), 140 deletions(-) create mode 100644 ui/lib/test-timeout.js diff --git a/test/e2e/beta/fetch-mocks.js b/test/e2e/beta/fetch-mocks.js index ee2d54f24..bb1702555 100644 --- a/test/e2e/beta/fetch-mocks.js +++ b/test/e2e/beta/fetch-mocks.js @@ -1,4 +1,5 @@ module.exports = { ethGasBasic: JSON.stringify({'average': 85.0, 'fastestWait': 0.6, 'fastWait': 0.6, 'fast': 200.0, 'safeLowWait': 4.8, 'blockNum': 6648312, 'avgWait': 4.2, 'block_time': 15.516129032258064, 'speed': 0.7828720873342716, 'fastest': 400.0, 'safeLow': 80.0}), ethGasPredictTable: JSON.stringify([{'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.2632423756, 'pct_remaining5m': 0.0, 'sum': 7.029975, 'tx_atabove': 4136.0, 'hashpower_accepting': 10.4166666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.2, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.433788122, 'pct_remaining5m': 0.0, 'sum': 7.01731875, 'tx_atabove': 4136.0, 'hashpower_accepting': 10.9375, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.5, 'pct_mined_5m': 0.0, 'total_seen_5m': 84.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.433788122, 'pct_remaining5m': 0.0, 'sum': 7.01731875, 'tx_atabove': 4136.0, 'hashpower_accepting': 10.9375, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.7, 'pct_mined_5m': 0.0, 'total_seen_5m': 5.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.4638844302, 'pct_remaining5m': 0.0, 'sum': 7.01731875, 'tx_atabove': 4136.0, 'hashpower_accepting': 10.9375, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.8, 'pct_mined_5m': 0.0, 'total_seen_5m': 20.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.4839486356, 'pct_remaining5m': 0.0, 'sum': 7.01731875, 'tx_atabove': 4136.0, 'hashpower_accepting': 10.9375, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 1.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 8.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.7347512039, 'pct_remaining5m': 0.0, 'sum': 7.0046625, 'tx_atabove': 4136.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 52.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 17.0, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 1.0, 'sum': 7.0046625, 'tx_atabove': 4136.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.1, 'pct_mined_5m': 0.0, 'total_seen_5m': 97.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 20.0, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 1.0, 'sum': 7.0040625, 'tx_atabove': 4135.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 2.2, 'pct_mined_5m': 0.0, 'total_seen_5m': 433.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 68.0, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 0.0, 'sum': 6.9986625, 'tx_atabove': 4126.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 50.0, 'gasprice': 2.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 14.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 0.0, 'sum': 6.9980625, 'tx_atabove': 4125.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 4.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 20.0, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 37.0, 'sum': 6.9956625, 'tx_atabove': 4121.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 75.0, 'gasprice': 2.5, 'pct_mined_5m': 0.0, 'total_seen_5m': 45.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 79.0, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 0.0, 'sum': 6.9788625, 'tx_atabove': 4093.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 2.6, 'pct_mined_5m': 0.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 27.5, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 100.0, 'sum': 6.9764625, 'tx_atabove': 4089.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.7, 'pct_mined_5m': 0.0, 'total_seen_5m': 3.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 22.5, 'hashpower_accepting2': 7.7447833066, 'pct_remaining5m': 66.0, 'sum': 6.9740625, 'tx_atabove': 4085.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 2.8, 'pct_mined_5m': 0.0, 'total_seen_5m': 6.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 20.0, 'hashpower_accepting2': 7.7548154093, 'pct_remaining5m': 38.0, 'sum': 6.9686625, 'tx_atabove': 4076.0, 'hashpower_accepting': 11.4583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 2.9, 'pct_mined_5m': 2.0, 'total_seen_5m': 36.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 27.0, 'hashpower_accepting2': 11.5268860353, 'pct_remaining5m': 77.0, 'sum': 6.8307, 'tx_atabove': 4057.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 127.0, 'int2': 6.9238, 'pct_remaining30m': 48.0, 'gasprice': 3.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 322.0, 'pct_mined_30m': 39.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 67.0, 'hashpower_accepting2': 11.5268860353, 'pct_remaining5m': 100.0, 'sum': 6.5697, 'tx_atabove': 3622.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 85.0, 'int2': 6.9238, 'pct_remaining30m': 98.0, 'gasprice': 3.1, 'pct_mined_5m': 0.0, 'total_seen_5m': 79.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 71.0, 'hashpower_accepting2': 11.5268860353, 'pct_remaining5m': 100.0, 'sum': 6.4311, 'tx_atabove': 3391.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 3.2, 'pct_mined_5m': 0.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 62.0, 'hashpower_accepting2': 11.5268860353, 'pct_remaining5m': 100.0, 'sum': 6.4209, 'tx_atabove': 3374.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 92.0, 'gasprice': 3.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1472.0, 'hashpower_accepting2': 11.5569823435, 'pct_remaining5m': 100.0, 'sum': 6.3951, 'tx_atabove': 3331.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 29.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 3.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 27.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 307.0, 'hashpower_accepting2': 11.5670144462, 'pct_remaining5m': 100.0, 'sum': 6.1521, 'tx_atabove': 2926.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 3.7, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1399.0, 'hashpower_accepting2': 11.577046549, 'pct_remaining5m': 100.0, 'sum': 6.1395, 'tx_atabove': 2905.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 3.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1005.0, 'hashpower_accepting2': 11.5971107544, 'pct_remaining5m': 88.0, 'sum': 6.1035, 'tx_atabove': 2845.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 88.0, 'gasprice': 4.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1546.0, 'hashpower_accepting2': 11.6171749599, 'pct_remaining5m': null, 'sum': 5.6151, 'tx_atabove': 2031.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 4.1, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1065.0, 'hashpower_accepting2': 11.6171749599, 'pct_remaining5m': 100.0, 'sum': 5.5509, 'tx_atabove': 1924.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 4.3, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 459.0, 'hashpower_accepting2': 11.6171749599, 'pct_remaining5m': 50.0, 'sum': 5.5137, 'tx_atabove': 1862.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 4.4, 'pct_mined_5m': 0.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 298.0, 'hashpower_accepting2': 11.6171749599, 'pct_remaining5m': null, 'sum': 5.4903, 'tx_atabove': 1823.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 4.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 812.0, 'hashpower_accepting2': 11.6472712681, 'pct_remaining5m': 0.0, 'sum': 5.4831, 'tx_atabove': 1811.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 4.8, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 541.0, 'hashpower_accepting2': 11.6472712681, 'pct_remaining5m': 100.0, 'sum': 5.4375, 'tx_atabove': 1735.0, 'hashpower_accepting': 16.6666666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 4.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1134.0, 'hashpower_accepting2': 11.7375601926, 'pct_remaining5m': 100.0, 'sum': 5.41824375, 'tx_atabove': 1724.0, 'hashpower_accepting': 17.1875, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 5.0, 'pct_mined_5m': 0.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1958.0, 'hashpower_accepting2': 11.7676565008, 'pct_remaining5m': null, 'sum': 4.9567875, 'tx_atabove': 976.0, 'hashpower_accepting': 17.7083333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 5.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1203.5, 'hashpower_accepting2': 11.8077849117, 'pct_remaining5m': null, 'sum': 4.9507875, 'tx_atabove': 966.0, 'hashpower_accepting': 17.7083333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 5.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 677.5, 'hashpower_accepting2': 11.8378812199, 'pct_remaining5m': null, 'sum': 4.9141875, 'tx_atabove': 905.0, 'hashpower_accepting': 17.7083333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 100.0, 'gasprice': 5.5, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 250.99, 'avgdiff': 0, 'expectedWait': 1000.0, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 3.0, 'hashpower_accepting2': 13.3928571429, 'pct_remaining5m': 0.0, 'sum': 3.16120625, 'tx_atabove': 832.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 12.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.92, 'avgdiff': 1, 'expectedWait': 23.5990451154, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.0248796148, 'pct_remaining5m': 0.0, 'sum': 3.10120625, 'tx_atabove': 732.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 6.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.58, 'avgdiff': 1, 'expectedWait': 22.2247437161, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 14.1753611557, 'pct_remaining5m': 0.0, 'sum': 3.09640625, 'tx_atabove': 724.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.55, 'avgdiff': 1, 'expectedWait': 22.1183205662, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.3459069021, 'pct_remaining5m': 0.0, 'sum': 3.09580625, 'tx_atabove': 723.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 6.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.55, 'avgdiff': 1, 'expectedWait': 22.1050535543, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.3960674157, 'pct_remaining5m': 0.0, 'sum': 3.09460625, 'tx_atabove': 721.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0785433993, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.5465489567, 'pct_remaining5m': 0.0, 'sum': 3.09460625, 'tx_atabove': 721.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0785433993, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.5666131621, 'pct_remaining5m': null, 'sum': 3.09460625, 'tx_atabove': 721.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.6, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0785433993, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 14.6769662921, 'pct_remaining5m': null, 'sum': 3.09460625, 'tx_atabove': 721.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0785433993, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.7070626003, 'pct_remaining5m': null, 'sum': 3.09400625, 'tx_atabove': 720.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 6.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0653002466, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 14.7271268058, 'pct_remaining5m': 0.0, 'sum': 3.09400625, 'tx_atabove': 720.0, 'hashpower_accepting': 20.3125, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 6.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.54, 'avgdiff': 1, 'expectedWait': 22.0653002466, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 3.0, 'hashpower_accepting2': 15.4795345104, 'pct_remaining5m': 0.0, 'sum': 3.06749375, 'tx_atabove': 718.0, 'hashpower_accepting': 21.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.39, 'avgdiff': 1, 'expectedWait': 21.4879808804, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 15.5898876404, 'pct_remaining5m': 0.0, 'sum': 3.06089375, 'tx_atabove': 707.0, 'hashpower_accepting': 21.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.36, 'avgdiff': 1, 'expectedWait': 21.3466271869, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 15.5999197432, 'pct_remaining5m': null, 'sum': 3.06029375, 'tx_atabove': 706.0, 'hashpower_accepting': 21.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.35, 'avgdiff': 1, 'expectedWait': 21.3338230522, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 15.8507223114, 'pct_remaining5m': 0.0, 'sum': 3.05969375, 'tx_atabove': 705.0, 'hashpower_accepting': 21.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.35, 'avgdiff': 1, 'expectedWait': 21.3210265977, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 15.8607544141, 'pct_remaining5m': null, 'sum': 3.05909375, 'tx_atabove': 704.0, 'hashpower_accepting': 21.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 7.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 0.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 5.35, 'avgdiff': 1, 'expectedWait': 21.3082378187, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 18.86035313, 'pct_remaining5m': 0.0, 'sum': 2.8933625, 'tx_atabove': 702.0, 'hashpower_accepting': 28.125, 'hpa_coef2': -0.067, 'total_seen_30m': 30.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 37.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.53, 'avgdiff': 1, 'expectedWait': 18.053913939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 19.1011235955, 'pct_remaining5m': 0.0, 'sum': 2.85250625, 'tx_atabove': 655.0, 'hashpower_accepting': 28.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.35, 'avgdiff': 1, 'expectedWait': 17.331163684, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 19.1613162119, 'pct_remaining5m': 0.0, 'sum': 2.84890625, 'tx_atabove': 649.0, 'hashpower_accepting': 28.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.33, 'avgdiff': 1, 'expectedWait': 17.268883666, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 19.231540931, 'pct_remaining5m': 0.0, 'sum': 2.8097375, 'tx_atabove': 647.0, 'hashpower_accepting': 30.2083333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 8.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.17, 'avgdiff': 1, 'expectedWait': 16.6055586875, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 19.5224719101, 'pct_remaining5m': 0.0, 'sum': 2.777225, 'tx_atabove': 635.0, 'hashpower_accepting': 31.25, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 12.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.03, 'avgdiff': 1, 'expectedWait': 16.0743526708, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 19.7331460674, 'pct_remaining5m': 0.0, 'sum': 2.774225, 'tx_atabove': 630.0, 'hashpower_accepting': 31.25, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 4.02, 'avgdiff': 1, 'expectedWait': 16.0262018751, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 19.8033707865, 'pct_remaining5m': 0.0, 'sum': 2.72905625, 'tx_atabove': 618.0, 'hashpower_accepting': 32.8125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 3.84, 'avgdiff': 1, 'expectedWait': 15.3184234339, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 19.9638844302, 'pct_remaining5m': 0.0, 'sum': 2.6954, 'tx_atabove': 583.0, 'hashpower_accepting': 33.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 8.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 3.72, 'avgdiff': 1, 'expectedWait': 14.8114421454, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 23.6155698234, 'pct_remaining5m': 0.0, 'sum': 2.3937875, 'tx_atabove': 460.0, 'hashpower_accepting': 42.7083333333, 'hpa_coef2': -0.067, 'total_seen_30m': 43.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 120.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.75, 'avgdiff': 1, 'expectedWait': 10.9549071782, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 24.0268860353, 'pct_remaining5m': 0.0, 'sum': 2.30313125, 'tx_atabove': 330.0, 'hashpower_accepting': 43.2291666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 23.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.51, 'avgdiff': 1, 'expectedWait': 10.0054630618, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 24.1472712681, 'pct_remaining5m': 0.0, 'sum': 2.287475, 'tx_atabove': 325.0, 'hashpower_accepting': 43.75, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 9.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.47, 'avgdiff': 1, 'expectedWait': 9.8500349165, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 24.2174959872, 'pct_remaining5m': 0.0, 'sum': 2.2609625, 'tx_atabove': 323.0, 'hashpower_accepting': 44.7916666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 9.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.41, 'avgdiff': 1, 'expectedWait': 9.5923173304, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 24.3880417335, 'pct_remaining5m': 0.0, 'sum': 2.22239375, 'tx_atabove': 322.0, 'hashpower_accepting': 46.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.32, 'avgdiff': 1, 'expectedWait': 9.2293973144, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 24.5284911717, 'pct_remaining5m': 0.0, 'sum': 2.2091375, 'tx_atabove': 321.0, 'hashpower_accepting': 46.875, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.29, 'avgdiff': 1, 'expectedWait': 9.1078574773, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 24.7391653291, 'pct_remaining5m': 0.0, 'sum': 2.2073375, 'tx_atabove': 318.0, 'hashpower_accepting': 46.875, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 8.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.28, 'avgdiff': 1, 'expectedWait': 9.0914780797, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 24.9699036918, 'pct_remaining5m': 0.0, 'sum': 2.182025, 'tx_atabove': 318.0, 'hashpower_accepting': 47.9166666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.7, 'pct_mined_5m': 88.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.22, 'avgdiff': 1, 'expectedWait': 8.8642381788, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 2.0, 'hashpower_accepting2': 25.1203852327, 'pct_remaining5m': 0.0, 'sum': 2.16936875, 'tx_atabove': 318.0, 'hashpower_accepting': 48.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.8, 'pct_mined_5m': 75.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.2, 'avgdiff': 1, 'expectedWait': 8.7527571186, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 25.1705457464, 'pct_remaining5m': 0.0, 'sum': 2.1561125, 'tx_atabove': 317.0, 'hashpower_accepting': 48.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 9.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 2.17, 'avgdiff': 1, 'expectedWait': 8.637494048, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 36.3864365971, 'pct_remaining5m': 0.0, 'sum': 1.769825, 'tx_atabove': 306.0, 'hashpower_accepting': 64.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 353.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.0, 'pct_mined_5m': 99.0, 'total_seen_5m': 245.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.47, 'avgdiff': 1, 'expectedWait': 5.8698260519, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 36.536918138, 'pct_remaining5m': 0.0, 'sum': 1.733225, 'tx_atabove': 245.0, 'hashpower_accepting': 64.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.42, 'avgdiff': 1, 'expectedWait': 5.658874382, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 36.7576243981, 'pct_remaining5m': 0.0, 'sum': 1.733225, 'tx_atabove': 245.0, 'hashpower_accepting': 64.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.42, 'avgdiff': 1, 'expectedWait': 5.658874382, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 36.8378812199, 'pct_remaining5m': 0.0, 'sum': 1.732625, 'tx_atabove': 244.0, 'hashpower_accepting': 64.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.42, 'avgdiff': 1, 'expectedWait': 5.6554800758, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 36.8679775281, 'pct_remaining5m': 0.0, 'sum': 1.732025, 'tx_atabove': 243.0, 'hashpower_accepting': 64.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.42, 'avgdiff': 1, 'expectedWait': 5.6520878055, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 37.8109951846, 'pct_remaining5m': 0.0, 'sum': 1.69405625, 'tx_atabove': 243.0, 'hashpower_accepting': 66.1458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 12.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 53.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.37, 'avgdiff': 1, 'expectedWait': 5.4415081179, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 37.871187801, 'pct_remaining5m': 0.0, 'sum': 1.69285625, 'tx_atabove': 241.0, 'hashpower_accepting': 66.1458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.36, 'avgdiff': 1, 'expectedWait': 5.4349822245, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 38.1019261637, 'pct_remaining5m': 0.0, 'sum': 1.69285625, 'tx_atabove': 241.0, 'hashpower_accepting': 66.1458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.36, 'avgdiff': 1, 'expectedWait': 5.4349822245, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 38.1821829856, 'pct_remaining5m': 0.0, 'sum': 1.68565625, 'tx_atabove': 229.0, 'hashpower_accepting': 66.1458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 10.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.35, 'avgdiff': 1, 'expectedWait': 5.3959908897, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 40.7002407705, 'pct_remaining5m': 0.0, 'sum': 1.520525, 'tx_atabove': 228.0, 'hashpower_accepting': 72.9166666667, 'hpa_coef2': -0.067, 'total_seen_30m': 84.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 84.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.15, 'avgdiff': 1, 'expectedWait': 4.5746262436, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 40.8206260032, 'pct_remaining5m': 0.0, 'sum': 1.507325, 'tx_atabove': 206.0, 'hashpower_accepting': 72.9166666667, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.13, 'avgdiff': 1, 'expectedWait': 4.5146379708, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 40.8908507223, 'pct_remaining5m': 0.0, 'sum': 1.507325, 'tx_atabove': 206.0, 'hashpower_accepting': 72.9166666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.13, 'avgdiff': 1, 'expectedWait': 4.5146379708, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.4024879615, 'pct_remaining5m': 0.0, 'sum': 1.49466875, 'tx_atabove': 206.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 15.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4578596422, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.4827447833, 'pct_remaining5m': 0.0, 'sum': 1.49466875, 'tx_atabove': 206.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4578596422, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.6131621188, 'pct_remaining5m': 0.0, 'sum': 1.49406875, 'tx_atabove': 205.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 8.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4551857287, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.6332263242, 'pct_remaining5m': 0.0, 'sum': 1.49406875, 'tx_atabove': 205.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4551857287, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.753611557, 'pct_remaining5m': 0.0, 'sum': 1.49406875, 'tx_atabove': 205.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4551857287, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 41.7736757624, 'pct_remaining5m': null, 'sum': 1.49406875, 'tx_atabove': 205.0, 'hashpower_accepting': 73.4375, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 11.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.12, 'avgdiff': 1, 'expectedWait': 4.4551857287, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 44.7030497592, 'pct_remaining5m': 0.0, 'sum': 1.41813125, 'tx_atabove': 205.0, 'hashpower_accepting': 76.5625, 'hpa_coef2': -0.067, 'total_seen_30m': 96.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 39.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.04, 'avgdiff': 1, 'expectedWait': 4.1293964158, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 44.9036918138, 'pct_remaining5m': 0.0, 'sum': 1.399475, 'tx_atabove': 195.0, 'hashpower_accepting': 77.0833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.02, 'avgdiff': 1, 'expectedWait': 4.0530715456, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 45.0341091493, 'pct_remaining5m': null, 'sum': 1.38681875, 'tx_atabove': 195.0, 'hashpower_accepting': 77.6041666667, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 1.0, 'avgdiff': 1, 'expectedWait': 4.0020981056, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.1845906902, 'pct_remaining5m': 0.0, 'sum': 1.3735625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.125, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.9493953846, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.1946227929, 'pct_remaining5m': null, 'sum': 1.3735625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.125, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.99, 'avgdiff': 1, 'expectedWait': 3.9493953846, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.3752006421, 'pct_remaining5m': 0.0, 'sum': 1.36090625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.98, 'avgdiff': 1, 'expectedWait': 3.8997258274, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.4955858748, 'pct_remaining5m': 0.0, 'sum': 1.36090625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.98, 'avgdiff': 1, 'expectedWait': 3.8997258274, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.525682183, 'pct_remaining5m': null, 'sum': 1.36090625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.7, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.98, 'avgdiff': 1, 'expectedWait': 3.8997258274, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.5858747994, 'pct_remaining5m': 0.0, 'sum': 1.36090625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.98, 'avgdiff': 1, 'expectedWait': 3.8997258274, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 45.636035313, 'pct_remaining5m': 0.0, 'sum': 1.36090625, 'tx_atabove': 194.0, 'hashpower_accepting': 78.6458333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 12.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.98, 'avgdiff': 1, 'expectedWait': 3.8997258274, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 46.9903691814, 'pct_remaining5m': 0.0, 'sum': 1.31028125, 'tx_atabove': 194.0, 'hashpower_accepting': 80.7291666667, 'hpa_coef2': -0.067, 'total_seen_30m': 47.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 34.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.93, 'avgdiff': 1, 'expectedWait': 3.7072162202, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 47.5321027287, 'pct_remaining5m': 0.0, 'sum': 1.292825, 'tx_atabove': 186.0, 'hashpower_accepting': 81.25, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.91, 'avgdiff': 1, 'expectedWait': 3.6430636874, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 47.5621990369, 'pct_remaining5m': 0.0, 'sum': 1.292825, 'tx_atabove': 186.0, 'hashpower_accepting': 81.25, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.91, 'avgdiff': 1, 'expectedWait': 3.6430636874, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 47.632423756, 'pct_remaining5m': null, 'sum': 1.292825, 'tx_atabove': 186.0, 'hashpower_accepting': 81.25, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.91, 'avgdiff': 1, 'expectedWait': 3.6430636874, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.5, 'hashpower_accepting2': 48.1440609952, 'pct_remaining5m': 0.0, 'sum': 1.28016875, 'tx_atabove': 186.0, 'hashpower_accepting': 81.7708333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 21.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.9, 'avgdiff': 1, 'expectedWait': 3.5972467097, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 48.4550561798, 'pct_remaining5m': 0.0, 'sum': 1.2651125, 'tx_atabove': 182.0, 'hashpower_accepting': 82.2916666667, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.89, 'avgdiff': 1, 'expectedWait': 3.5434913565, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 55.9590690209, 'pct_remaining5m': 0.0, 'sum': 1.2398, 'tx_atabove': 182.0, 'hashpower_accepting': 83.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 253.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 212.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.87, 'avgdiff': 1, 'expectedWait': 3.4549224112, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 56.0593900482, 'pct_remaining5m': 0.0, 'sum': 1.226, 'tx_atabove': 159.0, 'hashpower_accepting': 83.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.4075719515, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 56.1095505618, 'pct_remaining5m': 0.0, 'sum': 1.226, 'tx_atabove': 159.0, 'hashpower_accepting': 83.3333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 13.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.86, 'avgdiff': 1, 'expectedWait': 3.4075719515, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.5, 'hashpower_accepting2': 59.6408507223, 'pct_remaining5m': 0.0, 'sum': 1.13740625, 'tx_atabove': 159.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 119.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 115.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1186688184, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 59.7311396469, 'pct_remaining5m': 0.0, 'sum': 1.13440625, 'tx_atabove': 154.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 14.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1093268319, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 59.8214285714, 'pct_remaining5m': 0.0, 'sum': 1.13440625, 'tx_atabove': 154.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1093268319, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 60.1524879615, 'pct_remaining5m': 0.0, 'sum': 1.13380625, 'tx_atabove': 153.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1074617954, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.1725521669, 'pct_remaining5m': 0.0, 'sum': 1.13320625, 'tx_atabove': 152.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1055978775, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.2528089888, 'pct_remaining5m': 0.0, 'sum': 1.13320625, 'tx_atabove': 152.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1055978775, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.6440609952, 'pct_remaining5m': 0.0, 'sum': 1.13320625, 'tx_atabove': 152.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1055978775, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.6641252006, 'pct_remaining5m': 0.0, 'sum': 1.13320625, 'tx_atabove': 152.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1055978775, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 60.6942215088, 'pct_remaining5m': null, 'sum': 1.13320625, 'tx_atabove': 152.0, 'hashpower_accepting': 86.9791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 14.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.78, 'avgdiff': 1, 'expectedWait': 3.1055978775, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 62.9113162119, 'pct_remaining5m': 0.0, 'sum': 1.0952375, 'tx_atabove': 152.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 65.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 48.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9898926986, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 63.4129213483, 'pct_remaining5m': 0.0, 'sum': 1.0910375, 'tx_atabove': 145.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 8.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9773614832, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 64.4161316212, 'pct_remaining5m': 0.0, 'sum': 1.0886375, 'tx_atabove': 141.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.75, 'avgdiff': 1, 'expectedWait': 2.9702243836, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 64.4863563403, 'pct_remaining5m': 0.0, 'sum': 1.0820375, 'tx_atabove': 130.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.6, 'pct_mined_5m': 50.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9506854521, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 66.1817817014, 'pct_remaining5m': 0.0, 'sum': 1.0820375, 'tx_atabove': 130.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 24.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 17.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9506854521, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 66.2620385233, 'pct_remaining5m': 0.0, 'sum': 1.0766375, 'tx_atabove': 121.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 15.8, 'pct_mined_5m': 83.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9347946943, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 67.2853130016, 'pct_remaining5m': 0.0, 'sum': 1.0766375, 'tx_atabove': 121.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 15.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 11.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9347946943, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.2953451043, 'pct_remaining5m': null, 'sum': 1.0748375, 'tx_atabove': 118.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.1, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9295168154, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.4458266453, 'pct_remaining5m': 0.0, 'sum': 1.0748375, 'tx_atabove': 118.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9295168154, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.455858748, 'pct_remaining5m': 0.0, 'sum': 1.0748375, 'tx_atabove': 118.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 16.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9295168154, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.6565008026, 'pct_remaining5m': 0.0, 'sum': 1.0748375, 'tx_atabove': 118.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.74, 'avgdiff': 1, 'expectedWait': 2.9295168154, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 67.7267255217, 'pct_remaining5m': 0.0, 'sum': 1.0742375, 'tx_atabove': 117.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9277596325, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 68.6697431782, 'pct_remaining5m': 0.0, 'sum': 1.0742375, 'tx_atabove': 117.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 42.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 16.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 27.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9277596325, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 68.8904494382, 'pct_remaining5m': 0.0, 'sum': 1.0718375, 'tx_atabove': 113.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 11.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9207414346, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 69.0308988764, 'pct_remaining5m': 0.0, 'sum': 1.0712375, 'tx_atabove': 112.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 8.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9189895153, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.0409309791, 'pct_remaining5m': null, 'sum': 1.0706375, 'tx_atabove': 111.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9172386469, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.0710272873, 'pct_remaining5m': 0.0, 'sum': 1.0706375, 'tx_atabove': 111.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 17.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9172386469, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.08105939, 'pct_remaining5m': 0.0, 'sum': 1.0706375, 'tx_atabove': 111.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 17.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9172386469, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.1011235955, 'pct_remaining5m': null, 'sum': 1.0706375, 'tx_atabove': 111.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9172386469, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.1111556982, 'pct_remaining5m': 0.0, 'sum': 1.0706375, 'tx_atabove': 111.0, 'hashpower_accepting': 88.5416666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 17.9, 'pct_mined_5m': 0.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.73, 'avgdiff': 1, 'expectedWait': 2.9172386469, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.7632423756, 'pct_remaining5m': 0.0, 'sum': 1.05798125, 'tx_atabove': 111.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': 16.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 16.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.8805500054, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.7732744783, 'pct_remaining5m': 0.0, 'sum': 1.05618125, 'tx_atabove': 108.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 18.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.875369679, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.8134028892, 'pct_remaining5m': 0.0, 'sum': 1.05618125, 'tx_atabove': 108.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 18.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.875369679, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.823434992, 'pct_remaining5m': 0.0, 'sum': 1.05618125, 'tx_atabove': 108.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 18.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.875369679, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.8535313002, 'pct_remaining5m': null, 'sum': 1.05618125, 'tx_atabove': 108.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.875369679, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 69.8735955056, 'pct_remaining5m': 0.0, 'sum': 1.05618125, 'tx_atabove': 108.0, 'hashpower_accepting': 89.0625, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 18.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.72, 'avgdiff': 1, 'expectedWait': 2.875369679, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.3150080257, 'pct_remaining5m': 0.0, 'sum': 1.043525, 'tx_atabove': 108.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 13.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 11.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8392076024, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.3350722311, 'pct_remaining5m': 0.0, 'sum': 1.042325, 'tx_atabove': 106.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 19.3, 'pct_mined_5m': 66.0, 'total_seen_5m': 3.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8358025967, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.3752006421, 'pct_remaining5m': 0.0, 'sum': 1.042325, 'tx_atabove': 106.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 19.4, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8358025967, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.3852327448, 'pct_remaining5m': null, 'sum': 1.042325, 'tx_atabove': 106.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.5, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8358025967, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.4052969502, 'pct_remaining5m': null, 'sum': 1.042325, 'tx_atabove': 106.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.8, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8358025967, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 70.8266452648, 'pct_remaining5m': 0.0, 'sum': 1.042325, 'tx_atabove': 106.0, 'hashpower_accepting': 89.5833333333, 'hpa_coef2': -0.067, 'total_seen_30m': 18.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 19.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 10.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.71, 'avgdiff': 1, 'expectedWait': 2.8358025967, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 75.2708667737, 'pct_remaining5m': 0.0, 'sum': 0.90310625, 'tx_atabove': 106.0, 'hashpower_accepting': 95.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 144.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 185.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.62, 'avgdiff': 1, 'expectedWait': 2.4672551317, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.3009630819, 'pct_remaining5m': null, 'sum': 0.89650625, 'tx_atabove': 95.0, 'hashpower_accepting': 95.3125, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.2, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.62, 'avgdiff': 1, 'expectedWait': 2.4510248666, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 75.3210272873, 'pct_remaining5m': 0.0, 'sum': 0.89650625, 'tx_atabove': 95.0, 'hashpower_accepting': 95.3125, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 20.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.62, 'avgdiff': 1, 'expectedWait': 2.4510248666, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 77.7387640449, 'pct_remaining5m': 0.0, 'sum': 0.88385, 'tx_atabove': 95.0, 'hashpower_accepting': 95.8333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 24.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.61, 'avgdiff': 1, 'expectedWait': 2.420199561, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 77.7688603531, 'pct_remaining5m': 0.0, 'sum': 0.88325, 'tx_atabove': 94.0, 'hashpower_accepting': 95.8333333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.61, 'avgdiff': 1, 'expectedWait': 2.4187478768, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 77.9093097913, 'pct_remaining5m': null, 'sum': 0.87059375, 'tx_atabove': 94.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 20.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3883285027, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 79.7752808989, 'pct_remaining5m': 0.0, 'sum': 0.87059375, 'tx_atabove': 94.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 39.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 36.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3883285027, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 79.7953451043, 'pct_remaining5m': 0.0, 'sum': 0.86819375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 21.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3826033871, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 79.8154093098, 'pct_remaining5m': null, 'sum': 0.86819375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.4, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3826033871, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 79.845505618, 'pct_remaining5m': 0.0, 'sum': 0.86819375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3826033871, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.3069823435, 'pct_remaining5m': 0.0, 'sum': 0.86819375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.3541666667, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 21.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.6, 'avgdiff': 1, 'expectedWait': 2.3826033871, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.8888443018, 'pct_remaining5m': 0.0, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 13.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 13.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.9089085072, 'pct_remaining5m': null, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.1, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.91894061, 'pct_remaining5m': 0.0, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 22.2, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.9289727127, 'pct_remaining5m': null, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.3, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.9490369181, 'pct_remaining5m': 0.0, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 22.5, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.9791332263, 'pct_remaining5m': 0.0, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.8, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 80.9891653291, 'pct_remaining5m': null, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 22.9, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 81.47070626, 'pct_remaining5m': 0.0, 'sum': 0.8555375, 'tx_atabove': 90.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 15.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 23.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 19.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.352638584, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.4907704655, 'pct_remaining5m': 0.0, 'sum': 0.8531375, 'tx_atabove': 86.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 23.1, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3469990216, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.5108346709, 'pct_remaining5m': 0.0, 'sum': 0.8525375, 'tx_atabove': 85.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 23.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3455912446, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.6011235955, 'pct_remaining5m': 0.0, 'sum': 0.8525375, 'tx_atabove': 85.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 23.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3455912446, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.621187801, 'pct_remaining5m': 0.0, 'sum': 0.8525375, 'tx_atabove': 85.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 23.9, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3455912446, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 81.8719903692, 'pct_remaining5m': 0.0, 'sum': 0.8525375, 'tx_atabove': 85.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 24.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3455912446, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.8820224719, 'pct_remaining5m': 0.0, 'sum': 0.8501375, 'tx_atabove': 81.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 24.3, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3399685755, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.9121187801, 'pct_remaining5m': 0.0, 'sum': 0.8501375, 'tx_atabove': 81.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 24.6, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3399685755, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 81.9321829856, 'pct_remaining5m': 0.0, 'sum': 0.8501375, 'tx_atabove': 81.0, 'hashpower_accepting': 96.875, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 24.7, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.59, 'avgdiff': 1, 'expectedWait': 2.3399685755, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 83.3868378812, 'pct_remaining5m': 0.0, 'sum': 0.83688125, 'tx_atabove': 80.0, 'hashpower_accepting': 97.3958333333, 'hpa_coef2': -0.067, 'total_seen_30m': 24.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 25.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 32.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.58, 'avgdiff': 1, 'expectedWait': 2.3091540608, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 83.6276083467, 'pct_remaining5m': 0.0, 'sum': 0.82128125, 'tx_atabove': 54.0, 'hashpower_accepting': 97.3958333333, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 26.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.57, 'avgdiff': 1, 'expectedWait': 2.2734107799, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 83.7379614767, 'pct_remaining5m': 0.0, 'sum': 0.82008125, 'tx_atabove': 52.0, 'hashpower_accepting': 97.3958333333, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 27.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.57, 'avgdiff': 1, 'expectedWait': 2.2706843231, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 83.9887640449, 'pct_remaining5m': 0.0, 'sum': 0.81948125, 'tx_atabove': 51.0, 'hashpower_accepting': 97.3958333333, 'hpa_coef2': -0.067, 'total_seen_30m': 13.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 28.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 7.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.57, 'avgdiff': 1, 'expectedWait': 2.2693223212, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 84.0690208668, 'pct_remaining5m': 0.0, 'sum': 0.806825, 'tx_atabove': 51.0, 'hashpower_accepting': 97.9166666667, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 29.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.56, 'avgdiff': 1, 'expectedWait': 2.240782197, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 85.8447030498, 'pct_remaining5m': 0.0, 'sum': 0.7809125, 'tx_atabove': 50.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 55.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 30.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 49.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.55, 'avgdiff': 1, 'expectedWait': 2.1834637674, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 85.9751203852, 'pct_remaining5m': 0.0, 'sum': 0.7743125, 'tx_atabove': 39.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 31.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.169100358, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.1356340289, 'pct_remaining5m': 0.0, 'sum': 0.7743125, 'tx_atabove': 39.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 32.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 4.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.169100358, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.2760834671, 'pct_remaining5m': 0.0, 'sum': 0.7743125, 'tx_atabove': 39.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 33.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.169100358, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.4165329053, 'pct_remaining5m': 0.0, 'sum': 0.7737125, 'tx_atabove': 38.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 6.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 34.0, 'pct_mined_5m': 60.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1677992881, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.5, 'hashpower_accepting2': 86.7475922953, 'pct_remaining5m': 0.0, 'sum': 0.7737125, 'tx_atabove': 38.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 10.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 35.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 17.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1677992881, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.7877207063, 'pct_remaining5m': 0.0, 'sum': 0.7725125, 'tx_atabove': 36.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 36.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1651994891, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.8579454254, 'pct_remaining5m': 0.0, 'sum': 0.7725125, 'tx_atabove': 36.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 37.0, 'pct_mined_5m': 66.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1651994891, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.908105939, 'pct_remaining5m': 0.0, 'sum': 0.7725125, 'tx_atabove': 36.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 38.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1651994891, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 86.9783306581, 'pct_remaining5m': 0.0, 'sum': 0.7725125, 'tx_atabove': 36.0, 'hashpower_accepting': 98.9583333333, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 39.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1651994891, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 87.4498394864, 'pct_remaining5m': 0.0, 'sum': 0.75985625, 'tx_atabove': 36.0, 'hashpower_accepting': 99.4791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 16.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 40.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 14.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1379688654, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 89.6167736758, 'pct_remaining5m': 0.0, 'sum': 0.75805625, 'tx_atabove': 33.0, 'hashpower_accepting': 99.4791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 112.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 41.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 57.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.54, 'avgdiff': 1, 'expectedWait': 2.1341239829, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 89.6869983949, 'pct_remaining5m': 0.0, 'sum': 0.75445625, 'tx_atabove': 27.0, 'hashpower_accepting': 99.4791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 42.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.1264549491, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 89.7070626003, 'pct_remaining5m': null, 'sum': 0.75385625, 'tx_atabove': 26.0, 'hashpower_accepting': 99.4791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 43.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.1251794588, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 89.9478330658, 'pct_remaining5m': 0.0, 'sum': 0.75385625, 'tx_atabove': 26.0, 'hashpower_accepting': 99.4791666667, 'hpa_coef2': -0.067, 'total_seen_30m': 9.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 44.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 9.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.1251794588, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.088282504, 'pct_remaining5m': 0.0, 'sum': 0.7394, 'tx_atabove': 23.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 8.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 45.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 5.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0946783304, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 90.4995987159, 'pct_remaining5m': 0.0, 'sum': 0.7394, 'tx_atabove': 23.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 17.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 46.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 19.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0946783304, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.5999197432, 'pct_remaining5m': 0.0, 'sum': 0.7382, 'tx_atabove': 21.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 47.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0921662239, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.6199839486, 'pct_remaining5m': null, 'sum': 0.7382, 'tx_atabove': 21.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 48.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0921662239, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 90.6500802568, 'pct_remaining5m': null, 'sum': 0.7382, 'tx_atabove': 21.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 49.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0921662239, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 94.1412520064, 'pct_remaining5m': 0.0, 'sum': 0.7382, 'tx_atabove': 21.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 62.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 50.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 71.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.53, 'avgdiff': 1, 'expectedWait': 2.0921662239, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 94.2114767255, 'pct_remaining5m': 0.0, 'sum': 0.7358, 'tx_atabove': 17.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 51.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0871510456, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 94.4422150883, 'pct_remaining5m': 0.0, 'sum': 0.7358, 'tx_atabove': 17.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 5.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 52.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0871510456, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 94.4723113965, 'pct_remaining5m': null, 'sum': 0.7358, 'tx_atabove': 17.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 54.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0871510456, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 94.4823434992, 'pct_remaining5m': null, 'sum': 0.7346, 'tx_atabove': 15.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 56.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0846479665, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 95.0140449438, 'pct_remaining5m': 0.0, 'sum': 0.7346, 'tx_atabove': 15.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 27.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 60.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 17.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0846479665, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 95.6059390048, 'pct_remaining5m': 0.0, 'sum': 0.7334, 'tx_atabove': 13.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 28.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 61.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 14.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0821478893, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 96.8699839486, 'pct_remaining5m': 0.0, 'sum': 0.7322, 'tx_atabove': 11.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 21.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 63.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 25.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0796508104, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 1.0, 'hashpower_accepting2': 98.3948635634, 'pct_remaining5m': 0.0, 'sum': 0.7298, 'tx_atabove': 7.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 83.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 64.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 38.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0746656331, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.4149277689, 'pct_remaining5m': 0.0, 'sum': 0.728, 'tx_atabove': 4.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 65.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0709345939, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.4951845907, 'pct_remaining5m': 0.0, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 66.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.5052166934, 'pct_remaining5m': null, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 69.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.5152487961, 'pct_remaining5m': null, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 70.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.5553772071, 'pct_remaining5m': 0.0, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 72.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.5754414125, 'pct_remaining5m': 0.0, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 73.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.5854735152, 'pct_remaining5m': 0.0, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 77.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.6055377207, 'pct_remaining5m': null, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 79.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.8663723917, 'pct_remaining5m': 0.0, 'sum': 0.7274, 'tx_atabove': 3.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 14.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 80.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 6.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0696924058, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.8864365971, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 84.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.9466292135, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 88.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 98.9566613162, 'pct_remaining5m': null, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 90.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.0469502408, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 4.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 91.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 3.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.1272070626, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 3.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 96.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.1372391653, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 97.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.1472712681, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 99.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.7090690209, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 23.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 100.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 22.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.8394863563, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 7.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 101.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.9097110754, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 2.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 120.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.9297752809, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': null, 'int2': 6.9238, 'pct_remaining30m': null, 'gasprice': 134.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 2.0, 'pct_mined_30m': null, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': 0.0, 'hashpower_accepting2': 99.9498394864, 'pct_remaining5m': 0.0, 'sum': 0.7268, 'tx_atabove': 2.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 137.0, 'pct_mined_5m': 100.0, 'total_seen_5m': 1.0, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0684509628, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}, {'intercept': 4.8015, 'age': null, 'hashpower_accepting2': 99.9699036918, 'pct_remaining5m': null, 'sum': 0.7256, 'tx_atabove': 0.0, 'hashpower_accepting': 100.0, 'hpa_coef2': -0.067, 'total_seen_30m': 1.0, 'int2': 6.9238, 'pct_remaining30m': 0.0, 'gasprice': 180.0, 'pct_mined_5m': null, 'total_seen_5m': null, 'pct_mined_30m': 100.0, 'tx_atabove_coef': 0.0006, 'average': 600, 'safelow': 600, 'nomine': 550, 'expectedTime': 0.52, 'avgdiff': 1, 'expectedWait': 2.0659703104, 'avgdiff_coef': -1.6459, 'hpa_coef': -0.0243}]), + gasExpress: JSON.stringify({'safeLow': 2.0, 'standard': 3.0, 'fast': 10.0, 'fastest': 52.0, 'block_time': 15, 'blockNum': 6693030}), } diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index d8f442fb1..c5b7f40e4 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -74,7 +74,9 @@ describe('MetaMask', function () { 'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasBasic + '\')) }); } else if ' + '(args[0] === "https://ethgasstation.info/json/predictTable.json") { return ' + - 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } ' + + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' + + '(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' + + 'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' + 'return window.fetch(...args); }' ) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index a804f7b64..b5b13c849 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -12,10 +12,13 @@ export default class GasModalPageContainer extends Component { 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, @@ -28,10 +31,26 @@ export default class GasModalPageContainer extends Component { 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 ( { customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), newTotalFiat, currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), + blockTime: getBasicGasEstimateBlockTime(state), gasPriceButtonGroupProps: { buttonDataLoading, defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), @@ -150,6 +154,8 @@ const mapDispatchToProps = dispatch => { hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), hideSidebar: () => dispatch(hideSidebar()), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), } } @@ -209,7 +215,7 @@ function getTxParams (state, transactionId) { return txData.txParams || pendingTxParams || { from: send.from, gas: send.gasLimit, - gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state), + gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), to: send.to, value: getSelectedToken(state) ? '0x0' : send.amount, } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 16f4b8cd7..22f2f02dd 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -3,14 +3,21 @@ 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 = { @@ -59,6 +66,8 @@ describe('GasModalPageContainer Component', function () { wrapper = shallow( 'mockupdateCustomGasPrice'} updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} customGasPrice={21} @@ -76,6 +85,24 @@ describe('GasModalPageContainer Component', function () { 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) @@ -106,7 +133,10 @@ describe('GasModalPageContainer Component', function () { it('should pass the correct renderTabs property to PageContainer', () => { sinon.stub(GP, 'renderTabs').returns('mockTabs') - const renderTabsWrapperTester = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + const renderTabsWrapperTester = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props() assert.equal(tabsComponent, 'mockTabs') GasModalPageContainer.prototype.renderTabs.restore() diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 1ed28f33e..ba2cfe282 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -73,6 +73,9 @@ describe('gas-modal-page-container container', () => { conversionRate: 50, }, gas: { + basicEstimates: { + blockTime: 12, + }, customData: { limit: 'aaaaaaaa', price: 'ffffffff', @@ -100,6 +103,7 @@ describe('gas-modal-page-container container', () => { customGasLimit: 2863311530, currentTimeEstimate: '~1 min 11 sec', newTotalFiat: '637.41', + blockTime: 12, customModalGasLimitInHex: 'aaaaaaaa', customModalGasPriceInHex: 'ffffffff', gasChartProps: { diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js index 7a3b58ffa..2e460f377 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js @@ -32,7 +32,7 @@ export default class ConfirmTransaction extends Component { setTransactionToConfirm: PropTypes.func, confirmTransaction: PropTypes.object, clearConfirmTransaction: PropTypes.func, - fetchBasicGasEstimates: PropTypes.func, + fetchBasicGasAndTimeEstimates: PropTypes.func, } getParamsTransactionId () { @@ -46,7 +46,7 @@ export default class ConfirmTransaction extends Component { send = {}, history, confirmTransaction: { txData: { id: transactionId } = {} }, - fetchBasicGasEstimates, + fetchBasicGasAndTimeEstimates, } = this.props if (!totalUnapprovedCount && !send.to) { @@ -55,7 +55,7 @@ export default class ConfirmTransaction extends Component { } if (!transactionId) { - fetchBasicGasEstimates() + fetchBasicGasAndTimeEstimates() this.setTransactionToConfirm() } } diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js index a7e6966af..46342dc76 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js @@ -6,7 +6,7 @@ import { clearConfirmTransaction, } from '../../../ducks/confirm-transaction.duck' import { - fetchBasicGasEstimates, + fetchBasicGasAndTimeEstimates, } from '../../../ducks/gas.duck' import ConfirmTransaction from './confirm-transaction.component' import { getTotalUnapprovedCount } from '../../../selectors' @@ -27,7 +27,7 @@ const mapDispatchToProps = dispatch => { return { setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), - fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), } } diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js index 39266e590..977f8ab3c 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js @@ -7,7 +7,7 @@ import { } from '../../send.selectors.js' import { getBasicGasEstimateLoadingStatus, - getRenderableEstimateDataForSmallButtons, + getRenderableEstimateDataForSmallButtonsFromGWEI, getDefaultActiveButtonIndex, } from '../../../../selectors/custom-gas' import { @@ -23,7 +23,7 @@ import SendGasRow from './send-gas-row.component' export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow) function mapStateToProps (state) { - const gasButtonInfo = getRenderableEstimateDataForSmallButtons(state) + const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state) const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, getGasPrice(state)) return { diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js index 766bf6cab..f0c82e4f7 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -42,7 +42,7 @@ proxyquire('../send-gas-row.container.js', { '../../../../actions': actionSpies, '../../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, - getRenderableEstimateDataForSmallButtons: (s) => `mockGasButtonInfo:${s}`, + getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`, getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length, }, '../../../../ducks/send.duck': sendDuckSpies, diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js index 301acb1db..9b512aaf6 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/send/send.component.js @@ -35,6 +35,7 @@ export default class SendTransactionScreen extends PersistentForm { selectedToken: PropTypes.object, tokenBalance: PropTypes.string, tokenContract: PropTypes.object, + fetchBasicGasEstimates: PropTypes.func, updateAndSetGasTotal: PropTypes.func, updateSendErrors: PropTypes.func, updateSendTokenBalance: PropTypes.func, @@ -164,9 +165,8 @@ export default class SendTransactionScreen extends PersistentForm { componentDidMount () { this.props.fetchBasicGasEstimates() - .then(basicEstimates => { + .then(() => { this.updateGas() - this.props.fetchGasEstimates(basicEstimates.blockTime) }) } diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js index ac804cf2a..402e4bbe5 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/send/send.container.js @@ -38,7 +38,6 @@ import { } from '../../ducks/send.duck' import { fetchBasicGasEstimates, - fetchGasEstimates, } from '../../ducks/gas.duck' import { calcGasTotal, @@ -109,6 +108,5 @@ function mapDispatchToProps (dispatch) { qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), - fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), } } diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index 71eb93d61..443c82af5 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -11,7 +11,7 @@ const { calcGasTotal, } = require('./send.utils') import { - getAveragePriceEstimateInHexWEI, + getFastPriceEstimateInHexWEI, } from '../../selectors/custom-gas' const selectors = { @@ -139,7 +139,7 @@ function getGasLimit (state) { } function getGasPrice (state) { - return state.metamask.send.gasPrice || getAveragePriceEstimateInHexWEI(state) + return state.metamask.send.gasPrice || getFastPriceEstimateInHexWEI(state) } function getGasPriceFromRecentBlocks (state) { diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js index 68d2fc47e..81955cc1d 100644 --- a/ui/app/components/send/tests/send-component.test.js +++ b/ui/app/components/send/tests/send-component.test.js @@ -3,17 +3,12 @@ import assert from 'assert' import proxyquire from 'proxyquire' import { shallow } from 'enzyme' import sinon from 'sinon' +import timeout from '../../../../lib/test-timeout' import SendHeader from '../send-header/send-header.container' import SendContent from '../send-content/send-content.component' import SendFooter from '../send-footer/send-footer.container' -function timeout (time) { - return new Promise((resolve, reject) => { - setTimeout(resolve, time || 1500) - }) -} - const mockBasicGasEstimates = { blockTime: 'mockBlockTime', } @@ -88,7 +83,7 @@ describe('Send Component', function () { }) describe('componentDidMount', () => { - it('should call props.fetchBasicGasEstimates', () => { + it('should call props.fetchBasicGasAndTimeEstimates', () => { propsMethodSpies.fetchBasicGasEstimates.resetHistory() assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0) wrapper.instance().componentDidMount() @@ -103,15 +98,6 @@ describe('Send Component', function () { await timeout(250) assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) }) - - it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasEstimates', 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('componentWillUnmount', () => { diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js index d34dba210..5334484db 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -27,7 +27,7 @@ export default class TransactionListItem extends PureComponent { tokenData: PropTypes.object, transaction: PropTypes.object, value: PropTypes.string, - fetchBasicGasEstimates: PropTypes.func, + fetchBasicGasAndTimeEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func, } @@ -71,8 +71,8 @@ export default class TransactionListItem extends PureComponent { } resubmit () { - const { transaction, retryTransaction, fetchBasicGasEstimates, fetchGasEstimates } = this.props - fetchBasicGasEstimates().then(basicEstimates => { + const { transaction, retryTransaction, fetchBasicGasAndTimeEstimates, fetchGasEstimates } = this.props + fetchBasicGasAndTimeEstimates().then(basicEstimates => { fetchGasEstimates(basicEstimates.blockTime) }).then(() => { retryTransaction(transaction) diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js index df408b36b..61ecb04d0 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -8,7 +8,7 @@ import { hexToDecimal } from '../../helpers/conversions.util' import { getTokenData } from '../../helpers/transactions.util' import { formatDate } from '../../util' import { - fetchBasicGasEstimates, + fetchBasicGasAndTimeEstimates, fetchGasEstimates, setCustomGasPrice, setCustomGasLimit, @@ -29,7 +29,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), retryTransaction: (transaction) => { diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 6ae927b12..ffd1e773f 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -19,6 +19,7 @@ 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 = { @@ -42,7 +43,9 @@ const initState = { basicEstimateIsLoading: true, gasEstimatesLoading: true, priceAndTimeEstimates: [], + basicPriceAndTimeEstimates: [], priceAndTimeEstimatesLastRetrieved: 0, + basicPriceAndTimeEstimatesLastRetrieved: 0, errors: {}, } @@ -118,6 +121,11 @@ export default function reducer ({ gas: gasState = initState }, action = {}) { ...newState, priceAndTimeEstimatesLastRetrieved: action.value, } + case SET_BASIC_API_ESTIMATES_LAST_RETRIEVED: + return { + ...newState, + basicPriceAndTimeEstimatesLastRetrieved: action.value, + } case RESET_CUSTOM_DATA: return { ...newState, @@ -159,9 +167,9 @@ export function fetchBasicGasEstimates () { return (dispatch) => { dispatch(basicGasEstimatesLoadingStarted()) - return fetch('https://ethgasstation.info/json/ethgasAPI.json', { + return fetch('https://dev.blockscale.net/api/gasexpress.json', { 'headers': {}, - 'referrer': 'http://ethgasstation.info/json/', + 'referrer': 'https://dev.blockscale.net/api/', 'referrerPolicy': 'no-referrer-when-downgrade', 'body': null, 'method': 'GET', @@ -169,23 +177,53 @@ export function fetchBasicGasEstimates () { ) .then(r => r.json()) .then(({ - average, - avgWait, - block_time: blockTime, - blockNum, + safeLow, + standard: average, fast, fastest, - fastestWait, - fastWait, - safeLow, - safeLowWait, - speed, + block_time: blockTime, + blockNum, }) => { const basicEstimates = { + safeLow, average, - avgWait, + 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, + avgWait, + block_time: blockTime, + blockNum, fast, fastest, fastestWait, @@ -193,7 +231,34 @@ export function fetchBasicGasEstimates () { safeLow, safeLowWait, speed, - } + }) => { + 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 @@ -301,6 +366,13 @@ export function setApiEstimatesLastRetrieved (retrievalTime) { } } +export function setBasicApiEstimatesLastRetrieved (retrievalTime) { + return { + type: SET_BASIC_API_ESTIMATES_LAST_RETRIEVED, + value: retrievalTime, + } +} + export function resetCustomGasState () { return { type: RESET_CUSTOM_GAS_STATE } } diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index 009758cde..dff154dea 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -19,7 +19,7 @@ const { setCustomGasTotal, setCustomGasErrors, resetCustomGasState, - fetchBasicGasEstimates, + fetchBasicGasAndTimeEstimates, gasEstimatesLoadingStarted, gasEstimatesLoadingFinished, setPricesAndTimeEstimates, @@ -100,6 +100,9 @@ describe('Gas Duck', () => { gasEstimatesLoading: true, priceAndTimeEstimates: [], priceAndTimeEstimatesLastRetrieved: 0, + basicPriceAndTimeEstimates: [], + basicPriceAndTimeEstimatesLastRetrieved: 0, + } const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED' @@ -114,6 +117,7 @@ describe('Gas Duck', () => { 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', () => { @@ -224,7 +228,7 @@ describe('Gas Duck', () => { ) }) - it('should set priceAndTimeEstimatesLastRetrieved when receivinga SET_API_ESTIMATES_LAST_RETRIEVED action', () => { + it('should set priceAndTimeEstimatesLastRetrieved when receiving a SET_API_ESTIMATES_LAST_RETRIEVED action', () => { assert.deepEqual( GasReducer(mockState, { type: SET_API_ESTIMATES_LAST_RETRIEVED, @@ -234,6 +238,16 @@ describe('Gas Duck', () => { ) }) + 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, { @@ -272,10 +286,14 @@ describe('Gas Duck', () => { }) }) - describe('fetchBasicGasEstimates', () => { + describe('fetchBasicGasAndTimeEstimates', () => { const mockDistpatch = sinon.spy() it('should call fetch with the expected params', async () => { - await fetchBasicGasEstimates()(mockDistpatch) + await fetchBasicGasAndTimeEstimates()(mockDistpatch, () => ({ gas: Object.assign( + {}, + initState, + { basicPriceAndTimeEstimatesLastRetrieved: 1000000 } + ) })) assert.deepEqual( mockDistpatch.getCall(0).args, [{ type: BASIC_GAS_ESTIMATE_LOADING_STARTED} ] @@ -294,8 +312,14 @@ describe('Gas Duck', () => { }, ] ) + 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: { @@ -314,7 +338,7 @@ describe('Gas Duck', () => { }] ) assert.deepEqual( - mockDistpatch.getCall(2).args, + mockDistpatch.getCall(3).args, [{ type: BASIC_GAS_ESTIMATE_LOADING_FINISHED }] ) }) diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index 1c577c1b4..f06fb710d 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -23,7 +23,9 @@ import { addHexPrefix } from 'ethereumjs-util' const selectors = { formatTimeEstimate, getAveragePriceEstimateInHexWEI, + getFastPriceEstimateInHexWEI, getBasicGasEstimateLoadingStatus, + getBasicGasEstimateBlockTime, getCustomGasErrors, getCustomGasLimit, getCustomGasPrice, @@ -33,7 +35,7 @@ const selectors = { getEstimatedGasTimes, getPriceAndTimeEstimates, getRenderableBasicEstimateData, - getRenderableEstimateDataForSmallButtons, + getRenderableEstimateDataForSmallButtonsFromGWEI, priceEstimateToWei, } @@ -78,12 +80,21 @@ function getAveragePriceEstimateInHexWEI (state) { return getGasPriceInHexWei(averagePriceEstimate || '0x0') } +function getFastPriceEstimateInHexWEI (state, convertFromDecGWEI) { + const fastPriceEstimate = state.gas.basicEstimates.fast + return getGasPriceInHexWei(fastPriceEstimate || '0x0', convertFromDecGWEI) +} + function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { return gasButtonInfo.findIndex(({ priceInHexWei }) => { return priceInHexWei === addHexPrefix(customGasPriceInHex || gasPrice) }) } +function getBasicGasEstimateBlockTime (state) { + return state.gas.basicEstimates.blockTime +} + function apiEstimateModifiedToGWEI (estimate) { return multiplyCurrencies(estimate, 0.10, { toNumericBase: 'hex', @@ -102,26 +113,34 @@ function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) }) } -function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) { +function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9, convertFromDecGWEI) { + const initialConversion = convertFromDecGWEI + ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) + : apiEstimateModifiedToGWEI + return pipe( - apiEstimateModifiedToGWEI, + initialConversion, partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]), formatETHFee )(estimate, gasLimit) } -function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) { +function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate, convertFromDecGWEI) { + const initialConversion = convertFromDecGWEI + ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) + : apiEstimateModifiedToGWEI + return pipe( - apiEstimateModifiedToGWEI, + initialConversion, partialRight(basicPriceEstimateToETHTotal, [gasLimit]), partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), partialRight(formatCurrency, [convertedCurrency]) )(estimate, gasLimit, convertedCurrency, conversionRate) } -function getTimeEstimateInSeconds (blockWaitEstimate, currentBlockTime) { - return multiplyCurrencies(blockWaitEstimate, currentBlockTime, { +function getTimeEstimateInSeconds (blockWaitEstimate) { + return multiplyCurrencies(blockWaitEstimate, 60, { toNumericBase: 'dec', multiplicandBase: 10, multiplierBase: 10, @@ -141,11 +160,11 @@ function formatTimeEstimate (totalSeconds) { return formattedCombined } -function getRenderableTimeEstimate (blockWaitEstimate, currentBlockTime) { +function getRenderableTimeEstimate (blockWaitEstimate) { return pipe( getTimeEstimateInSeconds, formatTimeEstimate - )(blockWaitEstimate, currentBlockTime) + )(blockWaitEstimate) } function priceEstimateToWei (priceEstimate) { @@ -158,9 +177,13 @@ function priceEstimateToWei (priceEstimate) { }) } -function getGasPriceInHexWei (price) { +function getGasPriceInHexWei (price, convertFromDecGWEI) { + const initialConversion = convertFromDecGWEI + ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) + : apiEstimateModifiedToGWEI + return pipe( - apiEstimateModifiedToGWEI, + initialConversion, priceEstimateToWei, addHexPrefix )(price) @@ -177,11 +200,10 @@ function getRenderableBasicEstimateData (state) { gas: { basicEstimates: { safeLow, - average, fast, - blockTime, + fastest, safeLowWait, - avgWait, + fastestWait, fastWait, }, }, @@ -190,29 +212,29 @@ function getRenderableBasicEstimateData (state) { return [ { labelKey: 'fastest', - feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), - feeInSecondaryCurrency: getRenderableEthFee(fast, gasLimit), - timeEstimate: getRenderableTimeEstimate(fastWait, blockTime), - priceInHexWei: getGasPriceInHexWei(fast), + feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate), + feeInSecondaryCurrency: getRenderableEthFee(fastest, gasLimit), + timeEstimate: fastestWait && getRenderableTimeEstimate(fastestWait), + priceInHexWei: getGasPriceInHexWei(fastest), }, { labelKey: 'fast', - feeInPrimaryCurrency: getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate), - feeInSecondaryCurrency: getRenderableEthFee(average, gasLimit), - timeEstimate: getRenderableTimeEstimate(avgWait, blockTime), - priceInHexWei: getGasPriceInHexWei(average), + 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: getRenderableTimeEstimate(safeLowWait, blockTime), + timeEstimate: safeLowWait && getRenderableTimeEstimate(safeLowWait), priceInHexWei: getGasPriceInHexWei(safeLow), }, ] } -function getRenderableEstimateDataForSmallButtons (state) { +function getRenderableEstimateDataForSmallButtonsFromGWEI (state) { if (getBasicGasEstimateLoadingStatus(state)) { return [] } @@ -223,30 +245,30 @@ function getRenderableEstimateDataForSmallButtons (state) { gas: { basicEstimates: { safeLow, - average, fast, + fastest, }, }, } = state return [ { - labelKey: 'fast', - feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate), - feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS), - priceInHexWei: getGasPriceInHexWei(fast), + labelKey: 'fastest', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate, true), + feeInPrimaryCurrency: getRenderableEthFee(fastest, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), + priceInHexWei: getGasPriceInHexWei(fastest, true), }, { - labelKey: 'average', - feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate), - feeInPrimaryCurrency: getRenderableEthFee(average, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS), - priceInHexWei: getGasPriceInHexWei(average), + labelKey: 'fast', + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate, true), + 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), - priceInHexWei: getGasPriceInHexWei(safeLow), + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate, true), + feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), + priceInHexWei: getGasPriceInHexWei(safeLow, true), }, ] } diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js index 8a6e7e351..037b1e86e 100644 --- a/ui/app/selectors/tests/custom-gas.test.js +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -10,7 +10,7 @@ const { getEstimatedGasTimes, getPriceAndTimeEstimates, getRenderableBasicEstimateData, - getRenderableEstimateDataForSmallButtons, + getRenderableEstimateDataForSmallButtonsFromGWEI, } = proxyquire('../custom-gas', {}) describe('custom-gas selectors', () => { @@ -80,21 +80,21 @@ describe('custom-gas selectors', () => { labelKey: 'fastest', feeInPrimaryCurrency: '$0.05', feeInSecondaryCurrency: '0.00021 ETH', - timeEstimate: '~7 sec', + timeEstimate: '~30 sec', priceInHexWei: '0x2540be400', }, { labelKey: 'fast', feeInPrimaryCurrency: '$0.03', feeInSecondaryCurrency: '0.000105 ETH', - timeEstimate: '~46 sec', + timeEstimate: '~3 min 18 sec', priceInHexWei: '0x12a05f200', }, { labelKey: 'slow', feeInPrimaryCurrency: '$0.01', feeInSecondaryCurrency: '0.0000525 ETH', - timeEstimate: '~1 min 33 sec', + timeEstimate: '~6 min 36 sec', priceInHexWei: '0x9502f900', }, ], @@ -111,10 +111,10 @@ describe('custom-gas selectors', () => { blockTime: 14.16326530612245, safeLow: 25, safeLowWait: 6.6, - average: 50, - avgWait: 3.3, - fast: 100, - fastWait: 0.5, + fast: 50, + fastWait: 3.3, + fastest: 100, + fastestWait: 0.5, }, }, }, @@ -125,21 +125,21 @@ describe('custom-gas selectors', () => { labelKey: 'fastest', feeInPrimaryCurrency: '$1.07', feeInSecondaryCurrency: '0.00042 ETH', - timeEstimate: '~14 sec', + timeEstimate: '~1 min', priceInHexWei: '0x4a817c800', }, { labelKey: 'fast', feeInPrimaryCurrency: '$0.54', feeInSecondaryCurrency: '0.00021 ETH', - timeEstimate: '~1 min 33 sec', + timeEstimate: '~6 min 36 sec', priceInHexWei: '0x2540be400', }, { labelKey: 'slow', feeInPrimaryCurrency: '$0.27', feeInSecondaryCurrency: '0.000105 ETH', - timeEstimate: '~3 min 7 sec', + timeEstimate: '~13 min 12 sec', priceInHexWei: '0x12a05f200', }, ], @@ -156,10 +156,10 @@ describe('custom-gas selectors', () => { blockTime: 14.16326530612245, safeLow: 50, safeLowWait: 13.2, - average: 100, - avgWait: 6.6, - fast: 200, - fastWait: 1.0, + fast: 100, + fastWait: 6.6, + fastest: 200, + fastestWait: 1.0, }, }, }, @@ -176,27 +176,27 @@ describe('custom-gas selectors', () => { }) - describe('getRenderableEstimateDataForSmallButtons()', () => { + describe('getRenderableEstimateDataForSmallButtonsFromGWEI()', () => { const tests = [ { expectedResult: [ { - feeInSecondaryCurrency: '$0.05', - feeInPrimaryCurrency: '0.00021 ETH', - labelKey: 'fast', - priceInHexWei: '0x2540be400', + feeInSecondaryCurrency: '$0.54', + feeInPrimaryCurrency: '0.0021 ETH', + labelKey: 'fastest', + priceInHexWei: '0x174876e800', }, { - feeInSecondaryCurrency: '$0.03', - feeInPrimaryCurrency: '0.0001 ETH', - labelKey: 'average', - priceInHexWei: '0x12a05f200', + feeInSecondaryCurrency: '$0.27', + feeInPrimaryCurrency: '0.00105 ETH', + labelKey: 'fast', + priceInHexWei: '0xba43b7400', }, { - feeInSecondaryCurrency: '$0.01', - feeInPrimaryCurrency: '0.00005 ETH', + feeInSecondaryCurrency: '$0.13', + feeInPrimaryCurrency: '0.00052 ETH', labelKey: 'slow', - priceInHexWei: '0x9502f900', + priceInHexWei: '0x5d21dba00', }, ], mockState: { @@ -212,10 +212,10 @@ describe('custom-gas selectors', () => { blockTime: 14.16326530612245, safeLow: 25, safeLowWait: 6.6, - average: 50, - avgWait: 3.3, - fast: 100, - fastWait: 0.5, + fast: 50, + fastWait: 3.3, + fastest: 100, + fastestWait: 0.5, }, }, }, @@ -223,22 +223,22 @@ describe('custom-gas selectors', () => { { expectedResult: [ { - feeInSecondaryCurrency: '$1.07', - feeInPrimaryCurrency: '0.00042 ETH', - labelKey: 'fast', - priceInHexWei: '0x4a817c800', + feeInSecondaryCurrency: '$10.74', + feeInPrimaryCurrency: '0.0042 ETH', + labelKey: 'fastest', + priceInHexWei: '0x2e90edd000', }, { - feeInSecondaryCurrency: '$0.54', - feeInPrimaryCurrency: '0.00021 ETH', - labelKey: 'average', - priceInHexWei: '0x2540be400', + feeInSecondaryCurrency: '$5.37', + feeInPrimaryCurrency: '0.0021 ETH', + labelKey: 'fast', + priceInHexWei: '0x174876e800', }, { - feeInSecondaryCurrency: '$0.27', - feeInPrimaryCurrency: '0.0001 ETH', + feeInSecondaryCurrency: '$2.68', + feeInPrimaryCurrency: '0.00105 ETH', labelKey: 'slow', - priceInHexWei: '0x12a05f200', + priceInHexWei: '0xba43b7400', }, ], mockState: { @@ -254,10 +254,10 @@ describe('custom-gas selectors', () => { blockTime: 14.16326530612245, safeLow: 50, safeLowWait: 13.2, - average: 100, - avgWait: 6.6, - fast: 200, - fastWait: 1.0, + fast: 100, + fastWait: 6.6, + fastest: 200, + fastestWait: 1.0, }, }, }, @@ -266,7 +266,7 @@ describe('custom-gas selectors', () => { it('should return renderable data about basic estimates appropriate for buttons with less info', () => { tests.forEach(test => { assert.deepEqual( - getRenderableEstimateDataForSmallButtons(test.mockState), + getRenderableEstimateDataForSmallButtonsFromGWEI(test.mockState), test.expectedResult ) }) diff --git a/ui/lib/test-timeout.js b/ui/lib/test-timeout.js new file mode 100644 index 000000000..957b0fce2 --- /dev/null +++ b/ui/lib/test-timeout.js @@ -0,0 +1,5 @@ +export default function timeout (time) { + return new Promise((resolve, reject) => { + setTimeout(resolve, time || 1500) + }) +} From 7ffea926f23b2542c5182df7958defcdd9398b04 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 13:32:04 -0330 Subject: [PATCH 42/46] Add loading spinners when waiting for APIs in the gas customization modal --- .../advanced-tab-content.component.js | 8 +++++++- .../advanced-tab-content-component.test.js | 17 +++++++++++++++++ .../basic-tab-content.component.js | 16 +++++++++++----- .../tests/basic-tab-content-component.test.js | 11 +++++++++++ .../gas-modal-page-container.component.js | 2 ++ .../gas-modal-page-container.container.js | 4 ++++ .../gas-modal-page-container-component.test.js | 2 ++ .../gas-modal-page-container-container.test.js | 2 ++ ui/app/selectors/custom-gas.js | 5 +++++ 9 files changed, 61 insertions(+), 6 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 23945483d..ac68b833c 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -1,6 +1,7 @@ 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' export default class AdvancedTabContent extends Component { @@ -13,6 +14,7 @@ export default class AdvancedTabContent extends Component { updateCustomGasLimit: PropTypes.func, customGasPrice: PropTypes.number, customGasLimit: PropTypes.number, + gasEstimatesLoading: PropTypes.bool, millisecondsRemaining: PropTypes.number, totalFee: PropTypes.string, timeRemaining: PropTypes.string, @@ -98,6 +100,7 @@ export default class AdvancedTabContent extends Component { insufficientBalance, totalFee, gasChartProps, + gasEstimatesLoading, } = this.props return ( @@ -112,7 +115,10 @@ export default class AdvancedTabContent extends Component { insufficientBalance ) }
Live Gas Price Predictions
- + {!gasEstimatesLoading + ? + : + }
Slower Faster diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 0c25874bb..f321ca696 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -5,6 +5,7 @@ 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(), @@ -60,6 +61,22 @@ describe('AdvancedTabContent Component', function () { 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 diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 4483b71df..264d038da 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -1,5 +1,6 @@ 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 { @@ -12,15 +13,20 @@ export default class BasicTabContent extends Component { } render () { + const { gasPriceButtonGroupProps } = this.props + return (
Estimated Processing Times
Select a higher gas fee to accelerate the processing of your transaction.*
- + {!gasPriceButtonGroupProps.loading + ? + : + }
* 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.
) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js index 0c9c6ac63..25abdd997 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js @@ -4,6 +4,7 @@ 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, @@ -60,6 +61,7 @@ describe('BasicTabContent Component', function () { 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) @@ -67,5 +69,14 @@ describe('BasicTabContent Component', function () { 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) + }) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index b5b13c849..5d8f92a59 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -68,6 +68,7 @@ export default class GasModalPageContainer extends Component { gasChartProps, currentTimeEstimate, insufficientBalance, + gasEstimatesLoading, }) { const { transactionFee } = this.props return ( @@ -81,6 +82,7 @@ export default class GasModalPageContainer extends Component { totalFee={newTotalFiat} gasChartProps={gasChartProps} insufficientBalance={insufficientBalance} + gasEstimatesLoading={gasEstimatesLoading} /> ) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 770493be0..42b96a729 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -32,6 +32,7 @@ import { formatTimeEstimate, getFastPriceEstimateInHexWEI, getBasicGasEstimateLoadingStatus, + getGasEstimatesLoadingStatus, getCustomGasLimit, getCustomGasPrice, getDefaultActiveButtonIndex, @@ -65,6 +66,8 @@ import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price 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 @@ -127,6 +130,7 @@ const mapStateToProps = (state, ownProps) => { isSpeedUp: transaction.status === 'submitted', txId: transaction.id, insufficientBalance, + gasEstimatesLoading, } } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 22f2f02dd..2ba2fa9e7 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -232,6 +232,7 @@ describe('GasModalPageContainer Component', function () { customGasLimit: 456, newTotalFiat: '$0.30', currentTimeEstimate: '1 min 31 sec', + gasEstimatesLoading: 'mockGasEstimatesLoading', }) const advancedTabContentProps = renderAdvancedTabContentResult.props assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') @@ -240,6 +241,7 @@ describe('GasModalPageContainer Component', function () { assert.equal(advancedTabContentProps.customGasLimit, 456) assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec') assert.equal(advancedTabContentProps.totalFee, '$0.30') + assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading') }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index ba2cfe282..82c6dcd69 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -80,6 +80,7 @@ describe('gas-modal-page-container container', () => { limit: 'aaaaaaaa', price: 'ffffffff', }, + gasEstimatesLoading: false, priceAndTimeEstimates: [ { gasprice: 3, expectedTime: '31' }, { gasprice: 4, expectedTime: '62' }, @@ -118,6 +119,7 @@ describe('gas-modal-page-container container', () => { defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff', gasButtonInfo: 'mockRenderableBasicEstimateData:4', }, + gasEstimatesLoading: false, hideBasic: true, infoRowProps: { originalTotalFiat: '637.41', diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index f06fb710d..ec234500d 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -33,6 +33,7 @@ const selectors = { getDefaultActiveButtonIndex, getEstimatedGasPrices, getEstimatedGasTimes, + getGasEstimatesLoadingStatus, getPriceAndTimeEstimates, getRenderableBasicEstimateData, getRenderableEstimateDataForSmallButtonsFromGWEI, @@ -63,6 +64,10 @@ function getBasicGasEstimateLoadingStatus (state) { return state.gas.basicEstimateIsLoading } +function getGasEstimatesLoadingStatus (state) { + return state.gas.gasEstimatesLoading +} + function getPriceAndTimeEstimates (state) { return state.gas.priceAndTimeEstimates } From f8ffdaedc9a8fac631deafbc1d377430c980af94 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 14:06:35 -0330 Subject: [PATCH 43/46] Modify results of API data to better fit gas chart: remove outliers, pad data --- .../gas-price-chart.component.js | 11 ++- .../gas-price-chart/gas-price-chart.utils.js | 15 ++- .../tests/gas-price-chart.component.test.js | 2 +- ui/app/ducks/gas.duck.js | 93 +++++++++++++++++-- ui/app/ducks/tests/gas-duck.test.js | 49 +++++----- 5 files changed, 126 insertions(+), 44 deletions(-) diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js index 5ca465ba9..3f382e5b2 100644 --- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js @@ -33,16 +33,19 @@ export default class GasPriceChart extends Component { updateCustomGasPrice, }) { const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) - setTimeout(function () { setTickPosition('y', 0, -5, 8) setTickPosition('y', 1, -3, -5) - setTickPosition('x', 0, 3, 15) + setTickPosition('x', 0, 3) setTickPosition('x', 1, 3, -8) - // TODO: Confirm the below constants work with all data sets and screen sizes + 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(52, 2) rotate(-90)') + 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', () => { diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js index 67508703f..0b9dfbd11 100644 --- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -12,6 +12,7 @@ export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices if (currentPosValue === null && newTimeEstimate === null) { hideDataUI(chart, '#overlayed-circle') + return } const indexOfNewCircle = estimatedTimes.length + 1 @@ -162,7 +163,13 @@ export function setSelectedCircle ({ }) { const numberOfValues = chart.internal.data.xs.data1.length const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`) - const { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`) + let { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`) + + if (lowerX === higherX) { + const { x: higherXAdjusted, y: higherYAdjusted } = getCoordinateData(`.c3-circle-${closestHigherValueIndex + 1}`) + higherY = higherYAdjusted + higherX = higherXAdjusted + } const currentX = lowerX + (higherX - lowerX) * (newPrice - closestLowerValue) / (closestHigherValue - closestLowerValue) const newTimeEstimate = extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX }) @@ -203,13 +210,13 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate axis: { x: { min: gasPrices[0], - max: gasPricesMaxPadded, + max: gasPricesMax, tick: { - values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMaxPadded)], + values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], outer: false, format: function (val) { return val + ' GWEI' }, }, - padding: {left: gasPricesMaxPadded / 50, right: gasPricesMaxPadded / 50}, + padding: {left: gasPricesMax / 50, right: gasPricesMax / 50}, label: { text: 'Gas Price ($)', position: 'outer-center', diff --git a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js index 46341195b..74eddae42 100644 --- a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js +++ b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -136,7 +136,7 @@ describe('GasPriceChart Component', function () { 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, 15]) + assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3]) assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8]) }) diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index ffd1e773f..20a125dbb 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -1,4 +1,4 @@ -import { clone, uniqBy } from 'ramda' +import { clone, uniqBy, flatten } from 'ramda' import BigNumber from 'bignumber.js' import { loadLocalStorageData, @@ -266,6 +266,56 @@ export function fetchBasicGasAndTimeEstimates () { } } +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 { @@ -289,21 +339,50 @@ export function fetchGasEstimates (blockTime) { .then(r => { const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice })) const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes) - const timeMappedToSeconds = estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }) => { - const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toString(10) + + 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, - expectedWait, - gasprice, + gasprice: (new BigNumber(gasprice, 10).toNumber()), } }) const timeRetrieved = Date.now() dispatch(setApiEstimatesLastRetrieved(timeRetrieved)) saveLocalStorageData(timeRetrieved, 'GAS_API_ESTIMATES_LAST_RETRIEVED') - saveLocalStorageData(timeMappedToSeconds.slice(1), 'GAS_API_ESTIMATES') + saveLocalStorageData(timeMappedToSeconds, 'GAS_API_ESTIMATES') - return timeMappedToSeconds.slice(1) + return timeMappedToSeconds }) : Promise.resolve(priceAndTimeEstimates.length ? priceAndTimeEstimates diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index dff154dea..4783db30c 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -45,10 +45,25 @@ describe('Gas Duck', () => { 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 => { @@ -382,35 +397,13 @@ describe('Gas Duck', () => { [{ type: SET_API_ESTIMATES_LAST_RETRIEVED, value: 2000000 }] ) - assert.deepEqual( - mockDistpatch.getCall(2).args, - [{ - type: SET_PRICE_AND_TIME_ESTIMATES, - value: [ - { - expectedTime: '25', - expectedWait: 5, - gasprice: 2, - }, - { - expectedTime: '20', - expectedWait: 4, - gasprice: 4, - }, - { - expectedTime: '10', - expectedWait: 2, - gasprice: 10, - }, - { - expectedTime: '2.5', - expectedWait: 0.5, - gasprice: 20, - }, - ], + 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 }] From 75d75454374d98bf904b817bc2dd2b81b9e97a9d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Nov 2018 14:55:34 -0330 Subject: [PATCH 44/46] Clear custom gas data on hiding of gas customization modal. --- ui/app/components/modals/modal.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 46347312c..7ed0b6944 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -4,6 +4,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const FadeModal = require('boron').FadeModal const actions = require('../../actions') +const { resetCustomData: resetCustomGasData } = require('../../ducks/gas.duck') const isMobileView = require('../../../lib/is-mobile-view') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') @@ -317,6 +318,10 @@ const MODALS = { contentStyle: { borderRadius: '8px', }, + customOnHideOpts: { + action: resetCustomGasData, + args: [], + }, }, TRANSACTION_CONFIRMED: { @@ -392,8 +397,11 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - hideModal: () => { + hideModal: (customOnHideOpts) => { dispatch(actions.hideModal()) + if (customOnHideOpts.action) { + dispatch(customOnHideOpts.action(...customOnHideOpts.args)) + } }, hideWarning: () => { dispatch(actions.hideWarning()) @@ -425,7 +433,7 @@ Modal.prototype.render = function () { if (modal.onHide) { modal.onHide(this.props) } - this.onHide() + this.onHide(modal.customOnHideOpts) }, ref: (ref) => { this.modalRef = ref @@ -447,11 +455,11 @@ Modal.prototype.componentWillReceiveProps = function (nextProps) { } } -Modal.prototype.onHide = function () { +Modal.prototype.onHide = function (customOnHideOpts) { if (this.props.onHideCallback) { this.props.onHideCallback() } - this.props.hideModal() + this.props.hideModal(customOnHideOpts) } Modal.prototype.hide = function () { From d8e41a6aa5a4c64538063c6dde7afdf77b0e5793 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 27 Nov 2018 14:00:41 -0330 Subject: [PATCH 45/46] Final gas customization fixes --- .../transactions/tx-controller-test.js | 2 +- .../advanced-tab-content.component.js | 26 ++++++-- .../advanced-tab-content/index.scss | 1 + .../advanced-tab-content-component.test.js | 16 +++-- .../gas-modal-page-container.component.js | 3 +- .../gas-modal-page-container.container.js | 29 ++++++++- .../gas-modal-page-container/index.scss | 1 + ...gas-modal-page-container-container.test.js | 18 ++++-- .../gas-price-button-group/index.scss | 2 +- .../gas-price-chart.component.js | 2 +- .../gas-price-chart/gas-price-chart.utils.js | 54 ++++++++++------ ui/app/components/modals/modal.js | 2 +- .../page-container-footer.component.js | 2 +- .../components/sidebars/sidebar-content.scss | 40 +++++++++--- ui/app/ducks/gas.duck.js | 15 +++-- ui/app/ducks/tests/gas-duck.test.js | 16 ++--- ui/app/selectors/custom-gas.js | 61 ++++++++----------- ui/app/selectors/tests/custom-gas.test.js | 12 ++-- 18 files changed, 198 insertions(+), 104 deletions(-) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 6889f9bb2..74161e26c 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -413,7 +413,7 @@ describe('Transaction Controller', function () { gasPrice: '0xa', } txController.txStateManager._saveTxList([ - { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [] }, + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [{}] }, ]) expectedTxParams = Object.assign({}, txParams, { gasPrice: '0xb'}) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index ac68b833c..ba738ff75 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -3,6 +3,7 @@ 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 = { @@ -22,7 +23,21 @@ export default class AdvancedTabContent extends Component { insufficientBalance: PropTypes.bool, } - gasInput (value, onChange, min, insufficientBalance, precision, showGWEI) { + 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 (
onChange(Number(event.target.value))} />
-
onChange(value + 1)} />
-
onChange(value - 1)} />
+
onChange(value + 1)}>
+
onChange(value - 1)}>
{insufficientBalance &&
Insufficient Balance @@ -84,8 +98,8 @@ export default class AdvancedTabContent extends Component { renderGasEditRows (customGasPrice, updateCustomGasPrice, customGasLimit, updateCustomGasLimit, insufficientBalance) { return (
- { this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, 9, true) } - { this.renderGasEditRow('gasLimit', customGasLimit, updateCustomGasLimit, customGasLimit, insufficientBalance, 0) } + { this.renderGasEditRow('gasPrice', customGasPrice, updateCustomGasPrice, customGasPrice, insufficientBalance, true) } + { this.renderGasEditRow('gasLimit', customGasLimit, this.onChangeGasLimit, customGasLimit, insufficientBalance) }
) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index b62919c0a..88c69faf4 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -148,6 +148,7 @@ height: 100%; display: flex; justify-content: center; + cursor: pointer; } &__i-wrap:hover { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index f321ca696..d6920454d 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -155,8 +155,11 @@ describe('AdvancedTabContent Component', function () { 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', @@ -167,6 +170,10 @@ describe('AdvancedTabContent Component', function () { )) }) + afterEach(() => { + wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit + }) + it('should render the gas-edit-rows root node', () => { assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows')) }) @@ -182,10 +189,10 @@ describe('AdvancedTabContent Component', function () { const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args assert.equal(renderGasEditRowSpyArgs.length, 2) assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [ - 'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, 9, true, + 'gasPrice', 'mockGasPrice', () => 'mockUpdateCustomGasPriceReturn', 'mockGasPrice', false, true, ].map(String)) assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [ - 'gasLimit', 'mockGasLimit', () => 'mockUpdateCustomGasLimitReturn', 'mockGasLimit', false, 0, + 'gasLimit', 'mockGasLimit', () => 'mockOnChangeGasLimit', 'mockGasLimit', false, ].map(String)) }) }) @@ -234,7 +241,6 @@ describe('AdvancedTabContent Component', function () { const inputProps = gasInput.find('input').props() assert.equal(inputProps.min, 0) assert.equal(inputProps.value, 321) - assert.equal(inputProps.precision, 8) }) it('should call the passed onChange method with the value of the input onChange event', () => { @@ -257,9 +263,9 @@ describe('AdvancedTabContent Component', function () { 8, false )) - const upArrow = gasInput.find('.fa-angle-up') + 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('.fa-angle-down') + const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) assert.equal(downArrow.props().onClick(), 327) }) }) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 5d8f92a59..be91bef0f 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -89,7 +89,7 @@ export default class GasModalPageContainer extends Component { renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) { return ( -
+
{this.context.t('sendAmount')} @@ -167,7 +167,6 @@ export default class GasModalPageContainer extends Component { onClose={() => cancelAndClose()} onSubmit={() => { onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) - cancelAndClose() }} submitText={this.context.t('save')} headerCloseText={'Close'} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 42b96a729..c619a0988 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -142,6 +142,7 @@ const mapDispatchToProps = dispatch => { dispatch(resetCustomData()) dispatch(hideModal()) }, + hideModal: () => dispatch(hideModal()), updateCustomGasPrice, convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), @@ -150,6 +151,8 @@ const mapDispatchToProps = dispatch => { dispatch(setGasPrice(newPrice)) }, updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { + updateCustomGasPrice(gasPrice) + dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16)))) return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) }, createSpeedUpTransaction: (txId, gasPrice) => { @@ -172,6 +175,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, hideSidebar: dispatchHideSidebar, + cancelAndClose: dispatchCancelAndClose, + hideModal: dispatchHideModal, ...otherDispatchProps } = dispatchProps @@ -182,18 +187,27 @@ const mergeProps = (stateProps, dispatchProps, 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() + } + }, } } @@ -241,20 +255,29 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver } 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: currentGasPrice }) + } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) const newTimeEstimate = extrapolateY({ higherY: estimatedTimes[closestHigherValueIndex], lowerY: estimatedTimes[closestLowerValueIndex], higherX: closestHigherValue, lowerX: closestLowerValue, - xForExtrapolation: currentGasPrice, + xForExtrapolation: priceForEstimation, }) - return formatTimeEstimate(newTimeEstimate) + return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) } diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 2532c1fc2..6c76f1bdd 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -44,6 +44,7 @@ display: flex; justify-content: center; align-items: flex-start; + margin-right: 0; } &__subtitle { diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index 82c6dcd69..512832866 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -82,10 +82,10 @@ describe('gas-modal-page-container container', () => { }, gasEstimatesLoading: false, priceAndTimeEstimates: [ - { gasprice: 3, expectedTime: '31' }, - { gasprice: 4, expectedTime: '62' }, - { gasprice: 5, expectedTime: '93' }, - { gasprice: 6, expectedTime: '124' }, + { gasprice: 3, expectedTime: 31 }, + { gasprice: 4, expectedTime: 62 }, + { gasprice: 5, expectedTime: 93 }, + { gasprice: 6, expectedTime: 124 }, ], }, confirmTransaction: { @@ -235,7 +235,7 @@ describe('gas-modal-page-container container', () => { describe('updateConfirmTxGasAndCalculate()', () => { it('should dispatch a updateGasAndCalculate action with the correct props', () => { mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa') - assert(dispatchSpy.calledOnce) + assert.equal(dispatchSpy.callCount, 3) assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce) assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' }) }) @@ -265,6 +265,8 @@ describe('gas-modal-page-container container', () => { someOtherDispatchProp: sinon.spy(), createSpeedUpTransaction: sinon.spy(), hideSidebar: sinon.spy(), + hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), } ownProps = { someOwnProp: 123 } }) @@ -277,6 +279,7 @@ describe('gas-modal-page-container container', () => { 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) @@ -290,12 +293,14 @@ describe('gas-modal-page-container container', () => { 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() @@ -318,6 +323,7 @@ describe('gas-modal-page-container container', () => { 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') @@ -325,6 +331,7 @@ describe('gas-modal-page-container container', () => { 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() @@ -343,6 +350,7 @@ describe('gas-modal-page-container container', () => { 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) diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/gas-customization/gas-price-button-group/index.scss index 6e9d01c6e..0d1450594 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/index.scss +++ b/ui/app/components/gas-customization/gas-price-button-group/index.scss @@ -137,7 +137,7 @@ .gas-price-button-group--alt { display: flex; justify-content: stretch; - max-width: 342px; + width: 95%; &__button-fiat-price { font-size: 13px; diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js index 3f382e5b2..d4c67bbde 100644 --- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js @@ -19,7 +19,7 @@ export default class GasPriceChart extends Component { gasPrices: PropTypes.array, estimatedTimes: PropTypes.array, gasPricesMax: PropTypes.number, - estimatedTimesMax: PropTypes.string, + estimatedTimesMax: PropTypes.number, currentPrice: PropTypes.number, updateCustomGasPrice: PropTypes.func, } diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js index 0b9dfbd11..f19dafcc1 100644 --- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -1,5 +1,11 @@ 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({ @@ -24,7 +30,8 @@ export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices } export function getCoordinateData (selector) { - return d3.select(selector).node().getBoundingClientRect() + const node = d3.select(selector).node() + return node ? node.getBoundingClientRect() : {} } export function generateDataUIObj (x, index, value) { @@ -70,19 +77,22 @@ export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { } } -export function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) { - const slope = (higherY - lowerY) / (higherX - lowerX) - const newTimeEstimate = -1 * (slope * (higherX - xForExtrapolation) - higherY) +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 + return newTimeEstimate.toNumber() } export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { - const chartMouseXPos = xMousePos - chartXStart - const posPercentile = chartMouseXPos / chartWidth + const chartMouseXPos = bigNumMinus(xMousePos, chartXStart) + const posPercentile = bigNumDiv(chartMouseXPos, chartWidth) - const currentPosValue = (gasPrices[gasPrices.length - 1] - gasPrices[0]) * posPercentile + gasPrices[0] + const currentPosValue = (bigNumMinus(gasPrices[gasPrices.length - 1], gasPrices[0])) + .times(newBigSigDig(posPercentile)) + .plus(newBigSigDig(gasPrices[0])) + .toNumber() const { closestLowerValueIndex, @@ -162,20 +172,28 @@ export function setSelectedCircle ({ 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) { - const { x: higherXAdjusted, y: higherYAdjusted } = getCoordinateData(`.c3-circle-${closestHigherValueIndex + 1}`) - higherY = higherYAdjusted - higherX = higherXAdjusted + if (lowerX && higherX) { + while (lowerX === higherX) { + higherX = getCoordinateData(`.c3-circle-${count}`).x + higherY = getCoordinateData(`.c3-circle-${count}`).y + count++ + } } - const currentX = lowerX + (higherX - lowerX) * (newPrice - closestLowerValue) / (closestHigherValue - closestLowerValue) + 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, numberOfValues, newTimeEstimate), + generateDataUIObj(currentX.toNumber(), numberOfValues, newTimeEstimate), numberOfValues ) } @@ -282,8 +300,8 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate .style('margin-top', flipTooltip ? '-16px' : '4px') return { - top: circleY - chartYStart - 19 + (flipTooltip ? circleWidth + 38 : 0), - left: circleX - chartXStart + circleWidth - (gasPricesMaxPadded / 50), + 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, @@ -298,8 +316,8 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate appendOrUpdateCircle.bind(this)({ data, itemIndex, - cx: () => data.x - chartXStart + 11, - cy: () => data.value - chartYStart + 10, + cx: () => bigNumMinus(data.x, chartXStart).plus(11).toNumber(), + cy: () => bigNumMinus(data.value, chartYStart).plus(10).toNumber(), cssId: 'set-circle', appendOnly: true, }) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 7ed0b6944..0a603db4e 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -399,7 +399,7 @@ function mapDispatchToProps (dispatch) { return { hideModal: (customOnHideOpts) => { dispatch(actions.hideModal()) - if (customOnHideOpts.action) { + if (customOnHideOpts && customOnHideOpts.action) { dispatch(customOnHideOpts.action(...customOnHideOpts.args)) } }, diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js index 5725c22ac..85b16cefe 100644 --- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js @@ -12,7 +12,7 @@ export default class PageContainerFooter extends Component { submitText: PropTypes.string, disabled: PropTypes.bool, submitButtonType: PropTypes.string, - hideCancel: PropTypes.func, + hideCancel: PropTypes.bool, } static contextTypes = { diff --git a/ui/app/components/sidebars/sidebar-content.scss b/ui/app/components/sidebars/sidebar-content.scss index 7d1f719dc..ca6b0a458 100644 --- a/ui/app/components/sidebars/sidebar-content.scss +++ b/ui/app/components/sidebars/sidebar-content.scss @@ -1,6 +1,11 @@ .sidebar-left { + display: flex; + .gas-modal-page-container { + display: flex; + .page-container { + flex: 1; max-width: 100%; &__content { @@ -12,6 +17,10 @@ max-width: 344px; min-height: auto; } + + @media screen and (min-width: $break-small) { + max-height: none; + } } .gas-price-chart { @@ -34,9 +43,10 @@ } .basic-tab-content { - height: 318px; + height: auto; margin-bottom: 0px; border-bottom: 1px solid #d2d8dd; + flex: 1 1 70%; @media screen and (max-width: $break-small) { padding-left: 14px; @@ -55,6 +65,10 @@ } .advanced-tab { + @media screen and (min-width: $break-small) { + flex: 1 1 70%; + } + &__fee-chart { height: 320px; @@ -72,14 +86,26 @@ } } + .gas-modal-content { + display: flex; + flex-direction: column; + width: 100%; - .gas-modal-content__info-row { - height: 170px; - - @media screen and (max-width: $break-small) { - height: initial; + &__info-row-wrapper { display: flex; - justify-content: center; + @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; + } } } } diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 20a125dbb..8db24cc83 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -220,18 +220,25 @@ export function fetchBasicGasAndTimeEstimates () { ) .then(r => r.json()) .then(({ - average, + average: averageTimes10, avgWait, block_time: blockTime, blockNum, - fast, - fastest, + fast: fastTimes10, + fastest: fastestTimes10, fastestWait, fastWait, - safeLow, + 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, diff --git a/ui/app/ducks/tests/gas-duck.test.js b/ui/app/ducks/tests/gas-duck.test.js index 4783db30c..bf374c7ec 100644 --- a/ui/app/ducks/tests/gas-duck.test.js +++ b/ui/app/ducks/tests/gas-duck.test.js @@ -32,15 +32,15 @@ describe('Gas Duck', () => { let tempFetch let tempDateNow const mockEthGasApiResponse = { - average: 'mockAverage', + average: 20, avgWait: 'mockAvgWait', block_time: 'mockBlock_time', blockNum: 'mockBlockNum', - fast: 'mockFast', - fastest: 'mockFastest', + fast: 30, + fastest: 40, fastestWait: 'mockFastestWait', fastWait: 'mockFastWait', - safeLow: 'mockSafeLow', + safeLow: 10, safeLowWait: 'mockSafeLowWait', speed: 'mockSpeed', } @@ -338,15 +338,15 @@ describe('Gas Duck', () => { [{ type: SET_BASIC_GAS_ESTIMATE_DATA, value: { - average: 'mockAverage', + average: 2, avgWait: 'mockAvgWait', blockTime: 'mockBlock_time', blockNum: 'mockBlockNum', - fast: 'mockFast', - fastest: 'mockFastest', + fast: 3, + fastest: 4, fastestWait: 'mockFastestWait', fastWait: 'mockFastWait', - safeLow: 'mockSafeLow', + safeLow: 1, safeLowWait: 'mockSafeLowWait', speed: 'mockSpeed', }, diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index ec234500d..59f240f9c 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -85,9 +85,9 @@ function getAveragePriceEstimateInHexWEI (state) { return getGasPriceInHexWei(averagePriceEstimate || '0x0') } -function getFastPriceEstimateInHexWEI (state, convertFromDecGWEI) { +function getFastPriceEstimateInHexWEI (state) { const fastPriceEstimate = state.gas.basicEstimates.fast - return getGasPriceInHexWei(fastPriceEstimate || '0x0', convertFromDecGWEI) + return getGasPriceInHexWei(fastPriceEstimate || '0x0') } function getDefaultActiveButtonIndex (gasButtonInfo, customGasPriceInHex, gasPrice) { @@ -100,15 +100,6 @@ function getBasicGasEstimateBlockTime (state) { return state.gas.basicEstimates.blockTime } -function apiEstimateModifiedToGWEI (estimate) { - return multiplyCurrencies(estimate, 0.10, { - toNumericBase: 'hex', - multiplicandBase: 10, - multiplierBase: 10, - numberOfDecimals: 9, - }) -} - function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) { return conversionUtil(calcGasTotal(gasLimit, estimate), { fromNumericBase: 'hex', @@ -118,26 +109,18 @@ function basicPriceEstimateToETHTotal (estimate, gasLimit, numberOfDecimals = 9) }) } -function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9, convertFromDecGWEI) { - const initialConversion = convertFromDecGWEI - ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) - : apiEstimateModifiedToGWEI - +function getRenderableEthFee (estimate, gasLimit, numberOfDecimals = 9) { return pipe( - initialConversion, + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), partialRight(basicPriceEstimateToETHTotal, [gasLimit, numberOfDecimals]), formatETHFee )(estimate, gasLimit) } -function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate, convertFromDecGWEI) { - const initialConversion = convertFromDecGWEI - ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) - : apiEstimateModifiedToGWEI - +function getRenderableConvertedCurrencyFee (estimate, gasLimit, convertedCurrency, conversionRate) { return pipe( - initialConversion, + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), partialRight(basicPriceEstimateToETHTotal, [gasLimit]), partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), partialRight(formatCurrency, [convertedCurrency]) @@ -153,14 +136,26 @@ function getTimeEstimateInSeconds (blockWaitEstimate) { }) } -function formatTimeEstimate (totalSeconds) { +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 - ? `~${formattedMin} ${formattedSec}` - : '~' + [formattedMin, formattedSec].find(t => t) + ? `${symbol}${formattedMin} ${formattedSec}` + : symbol + [formattedMin, formattedSec].find(t => t) return formattedCombined } @@ -182,13 +177,9 @@ function priceEstimateToWei (priceEstimate) { }) } -function getGasPriceInHexWei (price, convertFromDecGWEI) { - const initialConversion = convertFromDecGWEI - ? x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }) - : apiEstimateModifiedToGWEI - +function getGasPriceInHexWei (price) { return pipe( - initialConversion, + x => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }), priceEstimateToWei, addHexPrefix )(price) @@ -259,19 +250,19 @@ function getRenderableEstimateDataForSmallButtonsFromGWEI (state) { return [ { labelKey: 'fastest', - feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(fastest, gasLimit, currentCurrency, conversionRate, true), + 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, true), + 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, true), + feeInSecondaryCurrency: getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate), feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit, NUMBER_OF_DECIMALS_SM_BTNS, true), priceInHexWei: getGasPriceInHexWei(safeLow, true), }, diff --git a/ui/app/selectors/tests/custom-gas.test.js b/ui/app/selectors/tests/custom-gas.test.js index 037b1e86e..ebc300160 100644 --- a/ui/app/selectors/tests/custom-gas.test.js +++ b/ui/app/selectors/tests/custom-gas.test.js @@ -109,11 +109,11 @@ describe('custom-gas selectors', () => { gas: { basicEstimates: { blockTime: 14.16326530612245, - safeLow: 25, + safeLow: 2.5, safeLowWait: 6.6, - fast: 50, + fast: 5, fastWait: 3.3, - fastest: 100, + fastest: 10, fastestWait: 0.5, }, }, @@ -154,11 +154,11 @@ describe('custom-gas selectors', () => { gas: { basicEstimates: { blockTime: 14.16326530612245, - safeLow: 50, + safeLow: 5, safeLowWait: 13.2, - fast: 100, + fast: 10, fastWait: 6.6, - fastest: 200, + fastest: 20, fastestWait: 1.0, }, }, From 8194309a9a7319bcebd6761a4596c208375adfab Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 3 Dec 2018 16:43:32 -0330 Subject: [PATCH 46/46] Fix styling of send screen in extension view when hex data on. --- .../gas-modal-page-container/index.scss | 6 ++++++ .../gas-price-button-group/index.scss | 12 ++++++++---- ui/app/components/page-container/index.scss | 7 ++----- ui/app/css/itcss/components/send.scss | 4 +--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/gas-customization/gas-modal-page-container/index.scss index 6c76f1bdd..efba24e02 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/index.scss +++ b/ui/app/components/gas-customization/gas-modal-page-container/index.scss @@ -5,9 +5,15 @@ .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 { diff --git a/ui/app/components/gas-customization/gas-price-button-group/index.scss b/ui/app/components/gas-customization/gas-price-button-group/index.scss index 0d1450594..c8b31fc83 100644 --- a/ui/app/components/gas-customization/gas-price-button-group/index.scss +++ b/ui/app/components/gas-customization/gas-price-button-group/index.scss @@ -67,10 +67,6 @@ justify-content: stretch; max-width: 260px; - @media screen and (max-width: $break-small) { - max-width: 230px; - } - &__button-fiat-price { font-size: 13px; } @@ -85,10 +81,18 @@ &__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 { diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss index 1634ab012..6fc97820a 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/page-container/index.scss @@ -6,6 +6,7 @@ display: flex; flex-flow: column; border-radius: 8px; + overflow-y: auto; &__header { display: flex; @@ -197,11 +198,7 @@ background-color: $white; border-radius: 0; flex: 1; - - &__content { - display: flex; - overflow-y: initial; - } + overflow-y: auto; } } diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index ba7db7aa8..19e840094 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -530,7 +530,6 @@ @media screen and (max-width: $break-small) { margin: 0; flex: 1 1 auto; - width: 100%; } } @@ -580,7 +579,7 @@ font-family: Roboto; font-size: 16px; line-height: 22px; - width: 112px; + width: 88px; font-weight: 400; flex: 0 0 auto; } @@ -623,7 +622,6 @@ &__to-autocomplete { position: relative; - max-width: 260px; &__down-caret { z-index: 1026;