Improved Gas Estimate UX (#830)

This commit is contained in:
James Prado 2018-01-15 04:59:59 -05:00 committed by Daniel Ternyak
parent 67b2e6491c
commit 6108d08693
34 changed files with 560 additions and 284 deletions

View File

@ -9,6 +9,13 @@ export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
}; };
} }
export type TToggleAutoGasLimit = typeof toggleAutoGasLimit;
export function toggleAutoGasLimit(): interfaces.ToggleAutoGasLimitAction {
return {
type: TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT
};
}
export type TChangeLanguage = typeof changeLanguage; export type TChangeLanguage = typeof changeLanguage;
export function changeLanguage(sign: string): interfaces.ChangeLanguageAction { export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
return { return {

View File

@ -6,6 +6,10 @@ export interface ToggleOfflineAction {
type: TypeKeys.CONFIG_TOGGLE_OFFLINE; type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
} }
export interface ToggleAutoGasLimitAction {
type: TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT;
}
/*** Change Language ***/ /*** Change Language ***/
export interface ChangeLanguageAction { export interface ChangeLanguageAction {
type: TypeKeys.CONFIG_LANGUAGE_CHANGE; type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
@ -74,6 +78,7 @@ export type ConfigAction =
| ChangeNodeAction | ChangeNodeAction
| ChangeLanguageAction | ChangeLanguageAction
| ToggleOfflineAction | ToggleOfflineAction
| ToggleAutoGasLimitAction
| PollOfflineStatus | PollOfflineStatus
| ChangeNodeIntentAction | ChangeNodeIntentAction
| AddCustomNodeAction | AddCustomNodeAction

View File

@ -3,6 +3,7 @@ export enum TypeKeys {
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE', CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT', CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
CONFIG_TOGGLE_AUTO_GAS_LIMIT = 'CONFIG_TOGGLE_AUTO_GAS_LIMIT',
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',

View File

@ -1,7 +1,8 @@
import { import {
TypeKeys,
EstimateGasFailedAction, EstimateGasFailedAction,
EstimateGasRequestedAction, EstimateGasRequestedAction,
TypeKeys, EstimateGasTimeoutAction,
EstimateGasSucceededAction, EstimateGasSucceededAction,
GetFromRequestedAction, GetFromRequestedAction,
GetFromSucceededAction, GetFromSucceededAction,
@ -29,6 +30,11 @@ const estimateGasFailed = (): EstimateGasFailedAction => ({
type: TypeKeys.ESTIMATE_GAS_FAILED type: TypeKeys.ESTIMATE_GAS_FAILED
}); });
type TEstimateGasTimedout = typeof estimateGasTimedout;
const estimateGasTimedout = (): EstimateGasTimeoutAction => ({
type: TypeKeys.ESTIMATE_GAS_TIMEDOUT
});
type TGetFromRequested = typeof getFromRequested; type TGetFromRequested = typeof getFromRequested;
const getFromRequested = (): GetFromRequestedAction => ({ const getFromRequested = (): GetFromRequestedAction => ({
type: TypeKeys.GET_FROM_REQUESTED type: TypeKeys.GET_FROM_REQUESTED
@ -63,6 +69,7 @@ const getNonceFailed = (): GetNonceFailedAction => ({
export { export {
estimateGasRequested, estimateGasRequested,
estimateGasFailed, estimateGasFailed,
estimateGasTimedout,
estimateGasSucceeded, estimateGasSucceeded,
getFromRequested, getFromRequested,
getFromSucceeded, getFromSucceeded,
@ -73,6 +80,7 @@ export {
TEstimateGasRequested, TEstimateGasRequested,
TEstimateGasFailed, TEstimateGasFailed,
TEstimateGasSucceeded, TEstimateGasSucceeded,
TEstimateGasTimedout,
TGetFromRequested, TGetFromRequested,
TGetFromSucceeded, TGetFromSucceeded,
TGetNonceRequested, TGetNonceRequested,

View File

@ -11,6 +11,9 @@ interface EstimateGasSucceededAction {
interface EstimateGasFailedAction { interface EstimateGasFailedAction {
type: TypeKeys.ESTIMATE_GAS_FAILED; type: TypeKeys.ESTIMATE_GAS_FAILED;
} }
interface EstimateGasTimeoutAction {
type: TypeKeys.ESTIMATE_GAS_TIMEDOUT;
}
interface GetFromRequestedAction { interface GetFromRequestedAction {
type: TypeKeys.GET_FROM_REQUESTED; type: TypeKeys.GET_FROM_REQUESTED;
} }
@ -36,6 +39,7 @@ type NetworkAction =
| EstimateGasFailedAction | EstimateGasFailedAction
| EstimateGasRequestedAction | EstimateGasRequestedAction
| EstimateGasSucceededAction | EstimateGasSucceededAction
| EstimateGasTimeoutAction
| GetFromRequestedAction | GetFromRequestedAction
| GetFromSucceededAction | GetFromSucceededAction
| GetFromFailedAction | GetFromFailedAction
@ -47,6 +51,7 @@ export {
EstimateGasRequestedAction, EstimateGasRequestedAction,
EstimateGasSucceededAction, EstimateGasSucceededAction,
EstimateGasFailedAction, EstimateGasFailedAction,
EstimateGasTimeoutAction,
GetFromRequestedAction, GetFromRequestedAction,
GetFromSucceededAction, GetFromSucceededAction,
GetFromFailedAction, GetFromFailedAction,

View File

@ -2,6 +2,7 @@ export enum TypeKeys {
ESTIMATE_GAS_REQUESTED = 'ESTIMATE_GAS_REQUESTED', ESTIMATE_GAS_REQUESTED = 'ESTIMATE_GAS_REQUESTED',
ESTIMATE_GAS_SUCCEEDED = 'ESTIMATE_GAS_SUCCEEDED', ESTIMATE_GAS_SUCCEEDED = 'ESTIMATE_GAS_SUCCEEDED',
ESTIMATE_GAS_FAILED = 'ESTIMATE_GAS_FAILED', ESTIMATE_GAS_FAILED = 'ESTIMATE_GAS_FAILED',
ESTIMATE_GAS_TIMEDOUT = 'ESTIMATE_GAS_TIMEDOUT',
GET_FROM_REQUESTED = 'GET_FROM_REQUESTED', GET_FROM_REQUESTED = 'GET_FROM_REQUESTED',
GET_FROM_SUCCEEDED = 'GET_FROM_SUCCEEDED', GET_FROM_SUCCEEDED = 'GET_FROM_SUCCEEDED',

View File

@ -1,22 +1,13 @@
import { DataFieldFactory } from './DataFieldFactory'; import { DataFieldFactory } from './DataFieldFactory';
import React from 'react'; import React from 'react';
import { Expandable, ExpandHandler } from 'components/ui';
import translate from 'translations'; import translate from 'translations';
import { donationAddressMap } from 'config/data'; import { donationAddressMap } from 'config/data';
const expander = (expandHandler: ExpandHandler) => (
<a onClick={expandHandler}>
<p className="strong">{translate('TRANS_advanced')}</p>
</a>
);
export const DataField: React.SFC<{}> = () => ( export const DataField: React.SFC<{}> = () => (
<DataFieldFactory <DataFieldFactory
withProps={({ data: { raw }, dataExists, onChange, readOnly }) => ( withProps={({ data: { raw }, dataExists, onChange, readOnly }) => (
<Expandable expandLabel={expander}> <>
<div className="form-group"> <label>{translate('OFFLINE_Step2_Label_6')}</label>
<label>{translate('TRANS_data')}</label>
<input <input
className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`} className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`}
type="text" type="text"
@ -25,8 +16,7 @@ export const DataField: React.SFC<{}> = () => (
readOnly={!!readOnly} readOnly={!!readOnly}
onChange={onChange} onChange={onChange}
/> />
</div> </>
</Expandable>
)} )}
/> />
); );

View File

@ -1,20 +1,45 @@
import React from 'react'; import React from 'react';
import { GasLimitFieldFactory } from './GasLimitFieldFactory'; import { GasLimitFieldFactory } from './GasLimitFieldFactory';
import translate from 'translations'; import translate from 'translations';
import { CSSTransition } from 'react-transition-group';
import { Spinner } from 'components/ui';
export const GasLimitField: React.SFC<{}> = () => ( interface Props {
includeLabel: boolean;
onlyIncludeLoader: boolean;
}
export const GaslimitLoading: React.SFC<{ gasEstimationPending: boolean }> = ({
gasEstimationPending
}) => (
<CSSTransition in={gasEstimationPending} timeout={300} classNames="fade">
<div className={`SimpleGas-estimating small ${gasEstimationPending ? 'active' : ''}`}>
Calculating gas limit
<Spinner />
</div>
</CSSTransition>
);
export const GasLimitField: React.SFC<Props> = ({ includeLabel, onlyIncludeLoader }) => (
<React.Fragment> <React.Fragment>
<label>{translate('TRANS_gas')} </label> {includeLabel ? <label>{translate('TRANS_gas')} </label> : null}
<GasLimitFieldFactory <GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => ( withProps={({ gasLimit: { raw, value }, onChange, readOnly, gasEstimationPending }) => (
<>
<GaslimitLoading gasEstimationPending={gasEstimationPending} />
{onlyIncludeLoader ? null : (
<input <input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`} className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="text" type="number"
placeholder="e.g. 21000"
readOnly={!!readOnly} readOnly={!!readOnly}
value={raw} value={raw}
onChange={onChange} onChange={onChange}
/> />
)} )}
</>
)}
/> />
</React.Fragment> </React.Fragment>
); );

View File

@ -10,6 +10,7 @@ const defaultGasLimit = '21000';
export interface CallBackProps { export interface CallBackProps {
readOnly: boolean; readOnly: boolean;
gasLimit: AppState['transaction']['fields']['gasLimit']; gasLimit: AppState['transaction']['fields']['gasLimit'];
gasEstimationPending: boolean;
onChange(value: React.FormEvent<HTMLInputElement>): void; onChange(value: React.FormEvent<HTMLInputElement>): void;
} }

View File

@ -2,11 +2,14 @@ import React, { Component } from 'react';
import { Query } from 'components/renderCbs'; import { Query } from 'components/renderCbs';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getGasLimit } from 'selectors/transaction'; import { getGasLimit, getGasEstimationPending } from 'selectors/transaction';
import { CallBackProps } from 'components/GasLimitFieldFactory'; import { CallBackProps } from 'components/GasLimitFieldFactory';
import { getAutoGasLimitEnabled } from 'selectors/config';
interface StateProps { interface StateProps {
gasLimit: AppState['transaction']['fields']['gasLimit']; gasLimit: AppState['transaction']['fields']['gasLimit'];
gasEstimationPending: boolean;
autoGasLimitEnabled: boolean;
} }
interface OwnProps { interface OwnProps {
@ -17,18 +20,24 @@ interface OwnProps {
type Props = StateProps & OwnProps; type Props = StateProps & OwnProps;
class GasLimitInputClass extends Component<Props> { class GasLimitInputClass extends Component<Props> {
public render() { public render() {
const { gasLimit, onChange } = this.props; const { gasLimit, onChange, gasEstimationPending, autoGasLimitEnabled } = this.props;
return ( return (
<Query <Query
params={['readOnly']} params={['readOnly']}
withQuery={({ readOnly }) => withQuery={({ readOnly }) =>
this.props.withProps({ gasLimit, onChange, readOnly: !!readOnly }) this.props.withProps({
gasLimit,
onChange,
readOnly: !!(readOnly || autoGasLimitEnabled),
gasEstimationPending
})
} }
/> />
); );
} }
} }
export const GasLimitInput = connect((state: AppState) => ({
export const GasLimitInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))( gasLimit: getGasLimit(state),
GasLimitInputClass gasEstimationPending: getGasEstimationPending(state),
); autoGasLimitEnabled: getAutoGasLimitEnabled(state)
}))(GasLimitInputClass);

View File

@ -1,37 +1,32 @@
import React from 'react'; import React from 'react';
import { translateRaw } from 'translations'; import { translateRaw } from 'translations';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { inputGasPrice, TInputGasPrice } from 'actions/transaction';
inputGasPrice,
TInputGasPrice,
inputGasLimit,
TInputGasLimit,
inputNonce,
TInputNonce
} from 'actions/transaction';
import { fetchCCRates, TFetchCCRates } from 'actions/rates'; import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig } from 'selectors/config'; import { getNetworkConfig, getOffline } from 'selectors/config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import SimpleGas from './components/SimpleGas'; import SimpleGas from './components/SimpleGas';
import AdvancedGas from './components/AdvancedGas'; import AdvancedGas from './components/AdvancedGas';
import './GasSlider.scss'; import './GasSlider.scss';
import { getGasPrice } from 'selectors/transaction';
interface Props { interface StateProps {
// Component configuration
disableAdvanced?: boolean;
// Data
gasPrice: AppState['transaction']['fields']['gasPrice']; gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit'];
nonce: AppState['transaction']['fields']['nonce'];
offline: AppState['config']['offline']; offline: AppState['config']['offline'];
network: AppState['config']['network']; network: AppState['config']['network'];
// Actions }
interface DispatchProps {
inputGasPrice: TInputGasPrice; inputGasPrice: TInputGasPrice;
inputGasLimit: TInputGasLimit;
inputNonce: TInputNonce;
fetchCCRates: TFetchCCRates; fetchCCRates: TFetchCCRates;
} }
interface OwnProps {
disableAdvanced?: boolean;
}
type Props = DispatchProps & OwnProps & StateProps;
interface State { interface State {
showAdvanced: boolean; showAdvanced: boolean;
} }
@ -54,22 +49,15 @@ class GasSlider extends React.Component<Props, State> {
} }
public render() { public render() {
const { gasPrice, gasLimit, nonce, offline, disableAdvanced } = this.props; const { offline, disableAdvanced, gasPrice } = this.props;
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced; const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
return ( return (
<div className="GasSlider"> <div className="GasSlider">
{showAdvanced ? ( {showAdvanced ? (
<AdvancedGas <AdvancedGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
gasPrice={gasPrice.raw}
gasLimit={gasLimit.raw}
nonce={nonce.raw}
changeGasPrice={this.props.inputGasPrice}
changeGasLimit={this.props.inputGasLimit}
changeNonce={this.props.inputNonce}
/>
) : ( ) : (
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} /> <SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
)} )}
{!offline && {!offline &&
@ -79,7 +67,7 @@ class GasSlider extends React.Component<Props, State> {
<strong> <strong>
{showAdvanced {showAdvanced
? `- ${translateRaw('Back to simple')}` ? `- ${translateRaw('Back to simple')}`
: `+ ${translateRaw('Advanced: Data, Gas Price, Gas Limit')}`} : `+ ${translateRaw('Advanced Settings')}`}
</strong> </strong>
</a> </a>
</div> </div>
@ -93,19 +81,15 @@ class GasSlider extends React.Component<Props, State> {
}; };
} }
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState): StateProps {
return { return {
gasPrice: state.transaction.fields.gasPrice, gasPrice: getGasPrice(state),
gasLimit: state.transaction.fields.gasLimit, offline: getOffline(state),
nonce: state.transaction.fields.nonce,
offline: state.config.offline,
network: getNetworkConfig(state) network: getNetworkConfig(state)
}; };
} }
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
inputGasPrice, inputGasPrice,
inputGasLimit,
inputNonce,
fetchCCRates fetchCCRates
})(GasSlider); })(GasSlider);

View File

@ -1,4 +1,31 @@
.AdvancedGas { .AdvancedGas {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
.checkbox {
display: flex;
align-items: center;
width: fit-content;
input[type='checkbox'] {
position: initial;
margin: 0;
margin-right: 8px;
}
span {
font-size: 1rem;
font-weight: 400;
}
}
&-gasLimit {
display: flex;
flex-wrap: wrap;
align-items: baseline;
.flex-spacer {
flex-grow: 2;
}
input {
width: 100%;
margin-top: 0;
}
}
} }

View File

@ -1,71 +1,69 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import translate from 'translations'; import translate from 'translations';
import { DataFieldFactory } from 'components/DataFieldFactory';
import FeeSummary from './FeeSummary'; import FeeSummary from './FeeSummary';
import './AdvancedGas.scss'; import './AdvancedGas.scss';
import { TToggleAutoGasLimit, toggleAutoGasLimit } from 'actions/config';
import { AppState } from 'reducers';
import { TInputGasPrice } from 'actions/transaction';
import { NonceField, GasLimitField, DataField } from 'components';
import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config';
interface Props { interface OwnProps {
gasPrice: string; inputGasPrice: TInputGasPrice;
gasLimit: string; gasPrice: AppState['transaction']['fields']['gasPrice'];
nonce: string;
changeGasPrice(gwei: string): void;
changeGasLimit(wei: string): void;
changeNonce(nonce: string): void;
} }
export default class AdvancedGas extends React.Component<Props> { interface StateProps {
public render() { autoGasLimitEnabled: AppState['config']['autoGasLimit'];
// Can't shadow var names for data & fee summary }
const vals = this.props;
interface DispatchProps {
toggleAutoGasLimit: TToggleAutoGasLimit;
}
type Props = OwnProps & StateProps & DispatchProps;
class AdvancedGas extends React.Component<Props> {
public render() {
const { autoGasLimitEnabled, gasPrice } = this.props;
return ( return (
<div className="AdvancedGas row form-group"> <div className="AdvancedGas row form-group">
<div className="col-md-12">
<label className="checkbox">
<input
type="checkbox"
defaultChecked={autoGasLimitEnabled}
onChange={this.handleToggleAutoGasLimit}
/>
<span>Automatically Calculate Gas Limit</span>
</label>
</div>
<div className="col-md-4 col-sm-6 col-xs-12"> <div className="col-md-4 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label> <label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input <input
className={classnames('form-control', !vals.gasPrice && 'is-invalid')} className={classnames('form-control', { 'is-invalid': !gasPrice.value })}
type="number" type="number"
placeholder="e.g. 40" placeholder="e.g. 40"
value={vals.gasPrice} value={gasPrice.raw}
onChange={this.handleGasPriceChange} onChange={this.handleGasPriceChange}
/> />
</div> </div>
<div className="col-md-4 col-sm-6 col-xs-12"> <div className="col-md-4 col-sm-6 col-xs-12 AdvancedGas-gasLimit">
<label>{translate('OFFLINE_Step2_Label_4')}</label> <label>{translate('OFFLINE_Step2_Label_4')}</label>
<input <div className="SimpleGas-flex-spacer" />
className={classnames('form-control', !vals.gasLimit && 'is-invalid')} <GasLimitField includeLabel={false} onlyIncludeLoader={false} />
type="number"
placeholder="e.g. 21000"
value={vals.gasLimit}
onChange={this.handleGasLimitChange}
/>
</div> </div>
<div className="col-md-4 col-sm-12"> <div className="col-md-4 col-sm-12">
<label>{translate('OFFLINE_Step2_Label_5')}</label> <NonceField alwaysDisplay={true} />
<input
className={classnames('form-control', !vals.nonce && 'is-invalid')}
type="number"
placeholder="e.g. 7"
value={vals.nonce}
onChange={this.handleNonceChange}
/>
</div> </div>
<div className="col-md-12"> <div className="col-md-12">
<label>{translate('OFFLINE_Step2_Label_6')}</label> <DataField />
<DataFieldFactory
withProps={({ data, onChange }) => (
<input
className="form-control"
value={data.raw}
onChange={onChange}
placeholder="0x7cB57B5A..."
/>
)}
/>
</div> </div>
<div className="col-sm-12"> <div className="col-sm-12">
@ -82,14 +80,15 @@ export default class AdvancedGas extends React.Component<Props> {
} }
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => { private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.changeGasPrice(ev.currentTarget.value); this.props.inputGasPrice(ev.currentTarget.value);
}; };
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => { private handleToggleAutoGasLimit = (_: React.FormEvent<HTMLInputElement>) => {
this.props.changeGasLimit(ev.currentTarget.value); this.props.toggleAutoGasLimit();
};
private handleNonceChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.changeNonce(ev.currentTarget.value);
}; };
} }
export default connect(
(state: AppState) => ({ autoGasLimitEnabled: getAutoGasLimitEnabled(state) }),
{ toggleAutoGasLimit }
)(AdvancedGas);

View File

@ -4,8 +4,24 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
&-label { &-flex-spacer {
display: block; flex-grow: 2;
}
&-title {
display: flex;
}
&-estimating {
color: rgba(51, 51, 51, 0.7);
display: flex;
align-items: baseline;
font-weight: 400;
opacity: 0;
&.active {
opacity: 1;
}
.Spinner {
margin-left: 8px;
}
} }
&-slider { &-slider {
@ -34,3 +50,26 @@
} }
} }
} }
.fade {
&-enter,
&-exit {
transition: opacity 300ms;
}
&-enter {
opacity: 0;
&-active {
opacity: 1;
}
}
&-exit {
opacity: 1;
&-active {
opacity: 0;
}
}
}

View File

@ -3,30 +3,55 @@ import Slider from 'rc-slider';
import translate from 'translations'; import translate from 'translations';
import { gasPriceDefaults } from 'config/data'; import { gasPriceDefaults } from 'config/data';
import FeeSummary from './FeeSummary'; import FeeSummary from './FeeSummary';
import { TInputGasPrice } from 'actions/transaction';
import './SimpleGas.scss'; import './SimpleGas.scss';
import { AppState } from 'reducers';
import { getGasLimitEstimationTimedOut } from 'selectors/transaction';
import { connect } from 'react-redux';
import { GasLimitField } from 'components/GasLimitField';
import { getIsWeb3Node } from 'selectors/config';
interface Props { interface OwnProps {
gasPrice: string; gasPrice: AppState['transaction']['fields']['gasPrice'];
changeGasPrice(gwei: string): void; inputGasPrice: TInputGasPrice;
} }
export default class SimpleGas extends React.Component<Props> { interface StateProps {
isWeb3Node: boolean;
gasLimitEstimationTimedOut: boolean;
}
type Props = OwnProps & StateProps;
class SimpleGas extends React.Component<Props> {
public render() { public render() {
const { gasPrice } = this.props; const { gasPrice, gasLimitEstimationTimedOut, isWeb3Node } = this.props;
return ( return (
<div className="SimpleGas row form-group"> <div className="SimpleGas row form-group">
<div className="col-md-12"> <div className="col-md-12 SimpleGas-title">
<label className="SimpleGas-label">{translate('Transaction Fee')}</label> <label className="SimpleGas-label">{translate('Transaction Fee')}</label>
<div className="SimpleGas-flex-spacer" />
<GasLimitField includeLabel={false} onlyIncludeLoader={true} />
</div> </div>
{gasLimitEstimationTimedOut && (
<div className="col-md-12 prompt-toggle-gas-limit">
<p className="small">
{isWeb3Node
? "Couldn't calculate gas limit, if you know what your doing, try setting manually in Advanced settings"
: "Couldn't calculate gas limit, try switching nodes"}
</p>
</div>
)}
<div className="col-md-8 col-sm-12"> <div className="col-md-8 col-sm-12">
<div className="SimpleGas-slider"> <div className="SimpleGas-slider">
<Slider <Slider
onChange={this.handleSlider} onChange={this.handleSlider}
min={gasPriceDefaults.gasPriceMinGwei} min={gasPriceDefaults.gasPriceMinGwei}
max={gasPriceDefaults.gasPriceMaxGwei} max={gasPriceDefaults.gasPriceMaxGwei}
value={parseFloat(gasPrice)} value={parseFloat(gasPrice.raw)}
/> />
<div className="SimpleGas-slider-labels"> <div className="SimpleGas-slider-labels">
<span>{translate('Cheap')}</span> <span>{translate('Cheap')}</span>
@ -49,6 +74,10 @@ export default class SimpleGas extends React.Component<Props> {
} }
private handleSlider = (gasGwei: number) => { private handleSlider = (gasGwei: number) => {
this.props.changeGasPrice(gasGwei.toString()); this.props.inputGasPrice(gasGwei.toString());
}; };
} }
export default connect((state: AppState) => ({
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
isWeb3Node: getIsWeb3Node(state)
}))(SimpleGas);

View File

@ -0,0 +1,38 @@
import React from 'react';
import { NonceFieldFactory } from 'components/NonceFieldFactory';
import Help from 'components/ui/Help';
interface Props {
alwaysDisplay: boolean;
}
const nonceHelp = (
<Help
size={'x1'}
link={'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'}
/>
);
export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
<NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
const content = (
<>
<label>Nonce</label>
{nonceHelp}
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 7"
value={raw}
readOnly={readOnly}
onChange={onChange}
/>
</>
);
return alwaysDisplay || shouldDisplay ? content : null;
}}
/>
);

View File

@ -1,23 +0,0 @@
import { NonceInput } from './NonceInput';
import { inputNonce, TInputNonce } from 'actions/transaction';
import React, { Component } from 'react';
import { connect } from 'react-redux';
interface DispatchProps {
inputNonce: TInputNonce;
}
class NonceFieldClass extends Component<DispatchProps> {
public render() {
return <NonceInput onChange={this.setNonce} />;
}
private setNonce = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputNonce(value);
};
}
export const NonceField = connect(null, {
inputNonce
})(NonceFieldClass);

View File

@ -1,54 +0,0 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import Help from 'components/ui/Help';
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
import { getOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
const nonceHelp = (
<Help
size={'x1'}
link={'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'}
/>
);
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
interface StateProps {
shouldDisplay: boolean;
nonce: AppState['transaction']['fields']['nonce'];
}
type Props = OwnProps & StateProps;
class NonceInputClass extends Component<Props> {
public render() {
const { nonce: { raw, value }, onChange, shouldDisplay } = this.props;
const content = (
<React.Fragment>
<label>Nonce</label>
{nonceHelp}
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="text"
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
/>
</React.Fragment>
);
return shouldDisplay ? content : null;
}
}
export const NonceInput = connect((state: AppState) => ({
shouldDisplay: getOffline(state) || nonceRequestFailed(state),
nonce: getNonce(state)
}))(NonceInputClass);

View File

@ -1 +0,0 @@
export * from './NonceField';

View File

@ -0,0 +1,37 @@
import { NonceInputFactory } from './NonceInputFactory';
import { inputNonce, TInputNonce } from 'actions/transaction';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
export interface CallbackProps {
nonce: AppState['transaction']['fields']['nonce'];
readOnly: boolean;
shouldDisplay: boolean;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
interface DispatchProps {
inputNonce: TInputNonce;
}
interface OwnProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = OwnProps & DispatchProps;
class NonceFieldClass extends Component<Props> {
public render() {
return <NonceInputFactory onChange={this.setNonce} withProps={this.props.withProps} />;
}
private setNonce = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputNonce(value);
};
}
export const NonceFieldFactory = connect(null, {
inputNonce
})(NonceFieldClass);

View File

@ -0,0 +1,39 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
import { getOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { CallbackProps } from 'components/NonceFieldFactory';
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
interface StateProps {
shouldDisplay: boolean;
nonce: AppState['transaction']['fields']['nonce'];
}
type Props = OwnProps & StateProps;
class NonceInputFactoryClass extends Component<Props> {
public render() {
const { nonce, onChange, shouldDisplay, withProps } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
withProps({ nonce, onChange, readOnly: !!readOnly, shouldDisplay })
}
/>
);
}
}
export const NonceInputFactory = connect((state: AppState) => ({
shouldDisplay: getOffline(state) || nonceRequestFailed(state),
nonce: getNonce(state)
}))(NonceInputFactoryClass);

View File

@ -0,0 +1 @@
export * from './NonceFieldFactory';

View File

@ -64,7 +64,7 @@ class DeployClass extends Component<DispatchProps> {
</label> </label>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-11">
<NonceField /> <NonceField alwaysDisplay={false} />
</div> </div>
</div> </div>
<div className="row form-group"> <div className="row form-group">

View File

@ -14,7 +14,7 @@ export class Fields extends Component<OwnProps> {
<React.Fragment> <React.Fragment>
<GasLimitField /> <GasLimitField />
<AmountField /> <AmountField />
<NonceField /> <NonceField alwaysDisplay={false} />
{this.props.button} {this.props.button}
<SigningStatus /> <SigningStatus />
<SendButton /> <SendButton />

View File

@ -106,7 +106,7 @@ class RequestPayment extends React.Component<Props, {}> {
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-11">
<GasLimitField /> <GasLimitField includeLabel={true} onlyIncludeLoader={false} />
</div> </div>
</div> </div>

View File

@ -46,10 +46,15 @@ export default class RpcNode implements INode {
} }
public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> { public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> {
// Timeout after 10 seconds
return this.client return this.client
.call(this.requests.estimateGas(transaction)) .call(this.requests.estimateGas(transaction))
.then(isValidEstimateGas) .then(isValidEstimateGas)
.then(({ result }) => Wei(result)); .then(({ result }) => Wei(result))
.catch(error => {
throw new Error(error.message);
});
} }
public getTokenBalance( public getTokenBalance(

View File

@ -28,6 +28,7 @@ export interface State {
network: NetworkConfig; network: NetworkConfig;
isChangingNode: boolean; isChangingNode: boolean;
offline: boolean; offline: boolean;
autoGasLimit: boolean;
customNodes: CustomNodeConfig[]; customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[]; customNetworks: CustomNetworkConfig[];
latestBlock: string; latestBlock: string;
@ -41,6 +42,7 @@ export const INITIAL_STATE: State = {
network: NETWORKS[NODES[defaultNode].network], network: NETWORKS[NODES[defaultNode].network],
isChangingNode: false, isChangingNode: false,
offline: false, offline: false,
autoGasLimit: true,
customNodes: [], customNodes: [],
customNetworks: [], customNetworks: [],
latestBlock: '???' latestBlock: '???'
@ -77,6 +79,13 @@ function toggleOffline(state: State): State {
}; };
} }
function toggleAutoGasLimitEstimation(state: State): State {
return {
...state,
autoGasLimit: !state.autoGasLimit
};
}
function addCustomNode(state: State, action: AddCustomNodeAction): State { function addCustomNode(state: State, action: AddCustomNodeAction): State {
const newId = makeCustomNodeId(action.payload); const newId = makeCustomNodeId(action.payload);
return { return {
@ -132,6 +141,8 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat
return changeNodeIntent(state); return changeNodeIntent(state);
case TypeKeys.CONFIG_TOGGLE_OFFLINE: case TypeKeys.CONFIG_TOGGLE_OFFLINE:
return toggleOffline(state); return toggleOffline(state);
case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT:
return toggleAutoGasLimitEstimation(state);
case TypeKeys.CONFIG_ADD_CUSTOM_NODE: case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
return addCustomNode(state, action); return addCustomNode(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE: case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:

View File

@ -26,6 +26,8 @@ export const network = (state: State = INITIAL_STATE, action: NetworkAction | Re
return nextState('gasEstimationStatus')(state, action); return nextState('gasEstimationStatus')(state, action);
case TK.ESTIMATE_GAS_FAILED: case TK.ESTIMATE_GAS_FAILED:
return nextState('gasEstimationStatus')(state, action); return nextState('gasEstimationStatus')(state, action);
case TK.ESTIMATE_GAS_TIMEDOUT:
return nextState('gasEstimationStatus')(state, action);
case TK.ESTIMATE_GAS_SUCCEEDED: case TK.ESTIMATE_GAS_SUCCEEDED:
return nextState('gasEstimationStatus')(state, action); return nextState('gasEstimationStatus')(state, action);
case TK.GET_FROM_REQUESTED: case TK.GET_FROM_REQUESTED:

View File

@ -1,7 +1,8 @@
export enum RequestStatus { export enum RequestStatus {
REQUESTED = 'PENDING', REQUESTED = 'PENDING',
SUCCEEDED = 'SUCCESS', SUCCEEDED = 'SUCCESS',
FAILED = 'FAIL' FAILED = 'FAIL',
TIMEDOUT = 'TIMEDOUT'
} }
export interface State { export interface State {
gasEstimationStatus: RequestStatus | null; gasEstimationStatus: RequestStatus | null;

View File

@ -1,13 +1,14 @@
import { SagaIterator, buffers, delay } from 'redux-saga'; import { SagaIterator, buffers, delay } from 'redux-saga';
import { apply, put, select, take, actionChannel, call, fork } from 'redux-saga/effects'; import { apply, put, select, take, actionChannel, call, fork, race } from 'redux-saga/effects';
import { INode } from 'libs/nodes/INode'; import { INode } from 'libs/nodes/INode';
import { getNodeLib, getOffline } from 'selectors/config'; import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet'; import { getWalletInst } from 'selectors/wallet';
import { getTransaction, IGetTransaction } from 'selectors/transaction'; import { getTransaction, IGetTransaction } from 'selectors/transaction';
import { import {
EstimateGasRequestedAction, EstimateGasRequestedAction,
setGasLimitField, setGasLimitField,
estimateGasFailed, estimateGasFailed,
estimateGasTimedout,
estimateGasSucceeded, estimateGasSucceeded,
TypeKeys, TypeKeys,
estimateGasRequested, estimateGasRequested,
@ -17,31 +18,36 @@ import {
SwapTokenToTokenAction, SwapTokenToTokenAction,
SwapTokenToEtherAction SwapTokenToEtherAction
} from 'actions/transaction'; } from 'actions/transaction';
import { TypeKeys as ConfigTypeKeys, ToggleAutoGasLimitAction } from 'actions/config';
import { IWallet } from 'libs/wallet'; import { IWallet } from 'libs/wallet';
import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction'; import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction';
export function* shouldEstimateGas(): SagaIterator { export function* shouldEstimateGas(): SagaIterator {
while (true) { while (true) {
const isOffline = yield select(getOffline);
if (isOffline) {
continue;
}
const action: const action:
| SetToFieldAction | SetToFieldAction
| SetDataFieldAction | SetDataFieldAction
| SwapEtherToTokenAction | SwapEtherToTokenAction
| SwapTokenToTokenAction | SwapTokenToTokenAction
| SwapTokenToEtherAction = yield take([ | SwapTokenToEtherAction
| ToggleAutoGasLimitAction = yield take([
TypeKeys.TO_FIELD_SET, TypeKeys.TO_FIELD_SET,
TypeKeys.DATA_FIELD_SET, TypeKeys.DATA_FIELD_SET,
TypeKeys.ETHER_TO_TOKEN_SWAP, TypeKeys.ETHER_TO_TOKEN_SWAP,
TypeKeys.TOKEN_TO_TOKEN_SWAP, TypeKeys.TOKEN_TO_TOKEN_SWAP,
TypeKeys.TOKEN_TO_ETHER_SWAP TypeKeys.TOKEN_TO_ETHER_SWAP,
ConfigTypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT
]); ]);
// invalid field is a field that the value is null and the input box isnt empty // invalid field is a field that the value is null and the input box isnt empty
// reason being is an empty field is valid because it'll be null // reason being is an empty field is valid because it'll be null
const isOffline: boolean = yield select(getOffline);
const autoGasLimitEnabled: boolean = yield select(getAutoGasLimitEnabled);
if (isOffline || !autoGasLimitEnabled) {
continue;
}
const invalidField = const invalidField =
(action.type === TypeKeys.TO_FIELD_SET || action.type === TypeKeys.DATA_FIELD_SET) && (action.type === TypeKeys.TO_FIELD_SET || action.type === TypeKeys.DATA_FIELD_SET) &&
!action.payload.value && !action.payload.value &&
@ -56,6 +62,7 @@ export function* shouldEstimateGas(): SagaIterator {
getTransactionFields, getTransactionFields,
transaction transaction
); );
yield put(estimateGasRequested(rest)); yield put(estimateGasRequested(rest));
} }
} }
@ -64,8 +71,10 @@ export function* estimateGas(): SagaIterator {
const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1)); const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1));
while (true) { while (true) {
const autoGasLimitEnabled: boolean = yield select(getAutoGasLimitEnabled);
const isOffline = yield select(getOffline); const isOffline = yield select(getOffline);
if (isOffline) {
if (isOffline || !autoGasLimitEnabled) {
continue; continue;
} }
@ -77,17 +86,28 @@ export function* estimateGas(): SagaIterator {
try { try {
const from: string = yield apply(walletInst, walletInst.getAddressString); const from: string = yield apply(walletInst, walletInst.getAddressString);
const txObj = { ...payload, from }; const txObj = { ...payload, from };
const gasLimit = yield apply(node, node.estimateGas, [txObj]); const { gasLimit } = yield race({
gasLimit: apply(node, node.estimateGas, [txObj]),
timeout: call(delay, 10000)
});
if (gasLimit) {
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit })); yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
yield put(estimateGasSucceeded()); yield put(estimateGasSucceeded());
} else {
yield put(estimateGasTimedout());
yield call(localGasEstimation, payload);
}
} catch (e) { } catch (e) {
yield put(estimateGasFailed()); yield put(estimateGasFailed());
// fallback for estimating locally yield call(localGasEstimation, payload);
const tx = yield call(makeTransaction, payload);
const gasLimit = yield apply(tx, tx.getBaseFee);
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
} }
} }
} }
export function* localGasEstimation(payload: EstimateGasRequestedAction['payload']) {
const tx = yield call(makeTransaction, payload);
const gasLimit = yield apply(tx, tx.getBaseFee);
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
}
export const gas = [fork(shouldEstimateGas), fork(estimateGas)]; export const gas = [fork(shouldEstimateGas), fork(estimateGas)];

View File

@ -16,6 +16,10 @@ export function getNode(state: AppState): string {
return state.config.nodeSelection; return state.config.nodeSelection;
} }
export function getIsWeb3Node(state: AppState): boolean {
return getNode(state) === 'web3';
}
export function getNodeConfig(state: AppState): NodeConfig { export function getNodeConfig(state: AppState): NodeConfig {
return state.config.node; return state.config.node;
} }
@ -86,6 +90,10 @@ export function getOffline(state: AppState): boolean {
return state.config.offline; return state.config.offline;
} }
export function getAutoGasLimitEnabled(state: AppState): boolean {
return state.config.autoGasLimit;
}
export function isSupportedUnit(state: AppState, unit: string) { export function isSupportedUnit(state: AppState, unit: string) {
const isToken: boolean = tokenExists(state, unit); const isToken: boolean = tokenExists(state, unit);
const isEther: boolean = isEtherUnit(unit); const isEther: boolean = isEtherUnit(unit);

View File

@ -2,10 +2,12 @@ import { AppState } from 'reducers';
import { getTransactionState } from 'selectors/transaction'; import { getTransactionState } from 'selectors/transaction';
import { RequestStatus } from 'reducers/transaction/network'; import { RequestStatus } from 'reducers/transaction/network';
const getNetworkStatus = (state: AppState) => getTransactionState(state).network; export const getNetworkStatus = (state: AppState) => getTransactionState(state).network;
const nonceRequestFailed = (state: AppState) =>
export const nonceRequestFailed = (state: AppState) =>
getNetworkStatus(state).getNonceStatus === RequestStatus.FAILED; getNetworkStatus(state).getNonceStatus === RequestStatus.FAILED;
const isNetworkRequestPending = (state: AppState) => {
export const isNetworkRequestPending = (state: AppState) => {
const network = getNetworkStatus(state); const network = getNetworkStatus(state);
const states: RequestStatus[] = Object.values(network); const states: RequestStatus[] = Object.values(network);
return states.reduce( return states.reduce(
@ -14,4 +16,8 @@ const isNetworkRequestPending = (state: AppState) => {
); );
}; };
export { nonceRequestFailed, isNetworkRequestPending }; export const getGasEstimationPending = (state: AppState) =>
getNetworkStatus(state).gasEstimationStatus === RequestStatus.REQUESTED;
export const getGasLimitEstimationTimedOut = (state: AppState) =>
getNetworkStatus(state).gasEstimationStatus === RequestStatus.TIMEDOUT;

View File

@ -133,7 +133,8 @@ const configureStore = () => {
nodeSelection: state.config.nodeSelection, nodeSelection: state.config.nodeSelection,
languageSelection: state.config.languageSelection, languageSelection: state.config.languageSelection,
customNodes: state.config.customNodes, customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks customNetworks: state.config.customNetworks,
setGasLimit: state.config.setGasLimit
}, },
transaction: { transaction: {
fields: { fields: {

View File

@ -1,6 +1,6 @@
import { buffers, delay } from 'redux-saga'; import { buffers, delay } from 'redux-saga';
import { apply, put, select, take, actionChannel, call } from 'redux-saga/effects'; import { apply, put, select, take, actionChannel, call, race } from 'redux-saga/effects';
import { getNodeLib, getOffline } from 'selectors/config'; import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet'; import { getWalletInst } from 'selectors/wallet';
import { getTransaction } from 'selectors/transaction'; import { getTransaction } from 'selectors/transaction';
import { import {
@ -8,15 +8,18 @@ import {
estimateGasFailed, estimateGasFailed,
estimateGasSucceeded, estimateGasSucceeded,
TypeKeys, TypeKeys,
estimateGasRequested estimateGasRequested,
estimateGasTimedout
} from 'actions/transaction'; } from 'actions/transaction';
import { makeTransaction, getTransactionFields } from 'libs/transaction'; import { makeTransaction, getTransactionFields } from 'libs/transaction';
import { shouldEstimateGas, estimateGas } from 'sagas/transaction/network/gas'; import { shouldEstimateGas, estimateGas, localGasEstimation } from 'sagas/transaction/network/gas';
import { cloneableGenerator } from 'redux-saga/utils'; import { cloneableGenerator } from 'redux-saga/utils';
import { Wei } from 'libs/units'; import { Wei } from 'libs/units';
import { TypeKeys as ConfigTypeKeys } from 'actions/config';
describe('shouldEstimateGas*', () => { describe('shouldEstimateGas*', () => {
const offline = false; const offline = false;
const autoGasLimitEnabled = true;
const transaction: any = 'transaction'; const transaction: any = 'transaction';
const tx = { transaction }; const tx = { transaction };
const rest: any = { const rest: any = {
@ -40,24 +43,29 @@ describe('shouldEstimateGas*', () => {
const gen = shouldEstimateGas(); const gen = shouldEstimateGas();
it('should select getOffline', () => {
expect(gen.next().value).toEqual(select(getOffline));
});
it('should take expected types', () => { it('should take expected types', () => {
expect(gen.next(offline).value).toEqual( expect(gen.next().value).toEqual(
take([ take([
TypeKeys.TO_FIELD_SET, TypeKeys.TO_FIELD_SET,
TypeKeys.DATA_FIELD_SET, TypeKeys.DATA_FIELD_SET,
TypeKeys.ETHER_TO_TOKEN_SWAP, TypeKeys.ETHER_TO_TOKEN_SWAP,
TypeKeys.TOKEN_TO_TOKEN_SWAP, TypeKeys.TOKEN_TO_TOKEN_SWAP,
TypeKeys.TOKEN_TO_ETHER_SWAP TypeKeys.TOKEN_TO_ETHER_SWAP,
ConfigTypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT
]) ])
); );
}); });
it('should select getOffline', () => {
expect(gen.next(action).value).toEqual(select(getOffline));
});
it('should select autoGasLimitEnabled', () => {
expect(gen.next(offline).value).toEqual(select(getAutoGasLimitEnabled));
});
it('should select getTransaction', () => { it('should select getTransaction', () => {
expect(gen.next(action).value).toEqual(select(getTransaction)); expect(gen.next(autoGasLimitEnabled).value).toEqual(select(getTransaction));
}); });
it('should call getTransactionFields with transaction', () => { it('should call getTransactionFields with transaction', () => {
@ -71,6 +79,7 @@ describe('shouldEstimateGas*', () => {
describe('estimateGas*', () => { describe('estimateGas*', () => {
const offline = false; const offline = false;
const autoGasLimitEnabled = true;
const requestChan = 'requestChan'; const requestChan = 'requestChan';
const payload: any = { const payload: any = {
mock1: 'mock1', mock1: 'mock1',
@ -86,9 +95,16 @@ describe('estimateGas*', () => {
const from = '0xa'; const from = '0xa';
const txObj = { ...payload, from }; const txObj = { ...payload, from };
const gasLimit = Wei('100'); const gasLimit = Wei('100');
const successfulGasEstimationResult = {
gasLimit
};
const gens: any = {}; const unsuccessfulGasEstimationResult = {
gens.gen = cloneableGenerator(estimateGas)(); gasLimit: null
};
const gens: { [name: string]: any } = {};
gens.successCase = cloneableGenerator(estimateGas)();
let random; let random;
beforeAll(() => { beforeAll(() => {
@ -104,41 +120,53 @@ describe('estimateGas*', () => {
const expected = JSON.stringify( const expected = JSON.stringify(
actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1)) actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1))
); );
const result = JSON.stringify(gens.gen.next().value); const result = JSON.stringify(gens.successCase.next().value);
expect(expected).toEqual(result); expect(expected).toEqual(result);
}); });
it('should select autoGasLimit', () => {
expect(gens.successCase.next(requestChan).value).toEqual(select(getAutoGasLimitEnabled));
});
it('should select getOffline', () => { it('should select getOffline', () => {
expect(gens.gen.next(requestChan).value).toEqual(select(getOffline)); expect(gens.successCase.next(autoGasLimitEnabled).value).toEqual(select(getOffline));
}); });
it('should take requestChan', () => { it('should take requestChan', () => {
expect(gens.gen.next(offline).value).toEqual(take(requestChan)); expect(gens.successCase.next(offline).value).toEqual(take(requestChan));
}); });
it('should call delay', () => { it('should call delay', () => {
expect(gens.gen.next(action).value).toEqual(call(delay, 250)); expect(gens.successCase.next(action).value).toEqual(call(delay, 250));
}); });
it('should select getNodeLib', () => { it('should select getNodeLib', () => {
expect(gens.gen.next().value).toEqual(select(getNodeLib)); expect(gens.successCase.next().value).toEqual(select(getNodeLib));
}); });
it('should select getWalletInst', () => { it('should select getWalletInst', () => {
expect(gens.gen.next(node).value).toEqual(select(getWalletInst)); expect(gens.successCase.next(node).value).toEqual(select(getWalletInst));
}); });
it('should apply walletInst', () => { it('should apply walletInst', () => {
expect(gens.gen.next(walletInst).value).toEqual(apply(walletInst, walletInst.getAddressString)); expect(gens.successCase.next(walletInst).value).toEqual(
apply(walletInst, walletInst.getAddressString)
);
}); });
it('should apply node.estimateGas', () => { it('should race between node.estimate gas and a 10 second timeout', () => {
gens.clone = gens.gen.clone(); gens.failCase = gens.successCase.clone();
expect(gens.gen.next(from).value).toEqual(apply(node, node.estimateGas, [txObj])); expect(gens.successCase.next(from).value).toEqual(
race({
gasLimit: apply(node, node.estimateGas, [txObj]),
timeout: call(delay, 10000)
})
);
}); });
it('should put setGasLimitField', () => { it('should put setGasLimitField', () => {
expect(gens.gen.next(gasLimit).value).toEqual( gens.timeOutCase = gens.successCase.clone();
expect(gens.successCase.next(successfulGasEstimationResult).value).toEqual(
put( put(
setGasLimitField({ setGasLimitField({
raw: gasLimit.toString(), raw: gasLimit.toString(),
@ -149,28 +177,56 @@ describe('estimateGas*', () => {
}); });
it('should put estimateGasSucceeded', () => { it('should put estimateGasSucceeded', () => {
expect(gens.gen.next().value).toEqual(put(estimateGasSucceeded())); expect(gens.successCase.next().value).toEqual(put(estimateGasSucceeded()));
});
describe('when it times out', () => {
it('should put estimateGasTimedout ', () => {
expect(gens.timeOutCase.next(unsuccessfulGasEstimationResult).value).toEqual(
put(estimateGasTimedout())
);
});
it('should call localGasEstimation', () => {
expect(gens.timeOutCase.next(estimateGasFailed()).value).toEqual(
call(localGasEstimation, payload)
);
});
}); });
describe('when it throws', () => { describe('when it throws', () => {
it('should catch and put estimateGasFailed', () => {
expect(gens.failCase.throw().value).toEqual(put(estimateGasFailed()));
});
it('should call localGasEstimation', () => {
expect(gens.failCase.next(estimateGasFailed()).value).toEqual(
call(localGasEstimation, payload)
);
});
});
});
describe('localGasEstimation', () => {
const payload: any = {
mock1: 'mock1',
mock2: 'mock2'
};
const tx = { const tx = {
getBaseFee: jest.fn() getBaseFee: jest.fn()
}; };
const gasLimit = Wei('100');
it('should catch and put estimateGasFailed', () => { const gen = localGasEstimation(payload);
expect(gens.clone.throw().value).toEqual(put(estimateGasFailed()));
});
it('should call makeTransaction with payload', () => { it('should call makeTransaction with payload', () => {
expect(gens.clone.next().value).toEqual(call(makeTransaction, payload)); expect(gen.next().value).toEqual(call(makeTransaction, payload));
}); });
it('should apply tx.getBaseFee', () => { it('should apply tx.getBaseFee', () => {
expect(gens.clone.next(tx).value).toEqual(apply(tx, tx.getBaseFee)); expect(gen.next(tx).value).toEqual(apply(tx, tx.getBaseFee));
}); });
it('should put setGasLimitField', () => { it('should put setGasLimitField', () => {
expect(gens.clone.next(gasLimit).value).toEqual( expect(gen.next(gasLimit).value).toEqual(
put( put(
setGasLimitField({ setGasLimitField({
raw: gasLimit.toString(), raw: gasLimit.toString(),
@ -180,4 +236,3 @@ describe('estimateGas*', () => {
); );
}); });
}); });
});