Improved Gas Estimate UX (#830)
This commit is contained in:
parent
67b2e6491c
commit
6108d08693
|
@ -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 function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
|
||||
return {
|
||||
|
|
|
@ -6,6 +6,10 @@ export interface ToggleOfflineAction {
|
|||
type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
|
||||
}
|
||||
|
||||
export interface ToggleAutoGasLimitAction {
|
||||
type: TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT;
|
||||
}
|
||||
|
||||
/*** Change Language ***/
|
||||
export interface ChangeLanguageAction {
|
||||
type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
|
||||
|
@ -74,6 +78,7 @@ export type ConfigAction =
|
|||
| ChangeNodeAction
|
||||
| ChangeLanguageAction
|
||||
| ToggleOfflineAction
|
||||
| ToggleAutoGasLimitAction
|
||||
| PollOfflineStatus
|
||||
| ChangeNodeIntentAction
|
||||
| AddCustomNodeAction
|
||||
|
|
|
@ -3,6 +3,7 @@ export enum TypeKeys {
|
|||
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
|
||||
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
|
||||
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_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
||||
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
TypeKeys,
|
||||
EstimateGasFailedAction,
|
||||
EstimateGasRequestedAction,
|
||||
TypeKeys,
|
||||
EstimateGasTimeoutAction,
|
||||
EstimateGasSucceededAction,
|
||||
GetFromRequestedAction,
|
||||
GetFromSucceededAction,
|
||||
|
@ -29,6 +30,11 @@ const estimateGasFailed = (): EstimateGasFailedAction => ({
|
|||
type: TypeKeys.ESTIMATE_GAS_FAILED
|
||||
});
|
||||
|
||||
type TEstimateGasTimedout = typeof estimateGasTimedout;
|
||||
const estimateGasTimedout = (): EstimateGasTimeoutAction => ({
|
||||
type: TypeKeys.ESTIMATE_GAS_TIMEDOUT
|
||||
});
|
||||
|
||||
type TGetFromRequested = typeof getFromRequested;
|
||||
const getFromRequested = (): GetFromRequestedAction => ({
|
||||
type: TypeKeys.GET_FROM_REQUESTED
|
||||
|
@ -63,6 +69,7 @@ const getNonceFailed = (): GetNonceFailedAction => ({
|
|||
export {
|
||||
estimateGasRequested,
|
||||
estimateGasFailed,
|
||||
estimateGasTimedout,
|
||||
estimateGasSucceeded,
|
||||
getFromRequested,
|
||||
getFromSucceeded,
|
||||
|
@ -73,6 +80,7 @@ export {
|
|||
TEstimateGasRequested,
|
||||
TEstimateGasFailed,
|
||||
TEstimateGasSucceeded,
|
||||
TEstimateGasTimedout,
|
||||
TGetFromRequested,
|
||||
TGetFromSucceeded,
|
||||
TGetNonceRequested,
|
||||
|
|
|
@ -11,6 +11,9 @@ interface EstimateGasSucceededAction {
|
|||
interface EstimateGasFailedAction {
|
||||
type: TypeKeys.ESTIMATE_GAS_FAILED;
|
||||
}
|
||||
interface EstimateGasTimeoutAction {
|
||||
type: TypeKeys.ESTIMATE_GAS_TIMEDOUT;
|
||||
}
|
||||
interface GetFromRequestedAction {
|
||||
type: TypeKeys.GET_FROM_REQUESTED;
|
||||
}
|
||||
|
@ -36,6 +39,7 @@ type NetworkAction =
|
|||
| EstimateGasFailedAction
|
||||
| EstimateGasRequestedAction
|
||||
| EstimateGasSucceededAction
|
||||
| EstimateGasTimeoutAction
|
||||
| GetFromRequestedAction
|
||||
| GetFromSucceededAction
|
||||
| GetFromFailedAction
|
||||
|
@ -47,6 +51,7 @@ export {
|
|||
EstimateGasRequestedAction,
|
||||
EstimateGasSucceededAction,
|
||||
EstimateGasFailedAction,
|
||||
EstimateGasTimeoutAction,
|
||||
GetFromRequestedAction,
|
||||
GetFromSucceededAction,
|
||||
GetFromFailedAction,
|
||||
|
|
|
@ -2,6 +2,7 @@ export enum TypeKeys {
|
|||
ESTIMATE_GAS_REQUESTED = 'ESTIMATE_GAS_REQUESTED',
|
||||
ESTIMATE_GAS_SUCCEEDED = 'ESTIMATE_GAS_SUCCEEDED',
|
||||
ESTIMATE_GAS_FAILED = 'ESTIMATE_GAS_FAILED',
|
||||
ESTIMATE_GAS_TIMEDOUT = 'ESTIMATE_GAS_TIMEDOUT',
|
||||
|
||||
GET_FROM_REQUESTED = 'GET_FROM_REQUESTED',
|
||||
GET_FROM_SUCCEEDED = 'GET_FROM_SUCCEEDED',
|
||||
|
|
|
@ -1,32 +1,22 @@
|
|||
import { DataFieldFactory } from './DataFieldFactory';
|
||||
import React from 'react';
|
||||
import { Expandable, ExpandHandler } from 'components/ui';
|
||||
import translate from 'translations';
|
||||
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<{}> = () => (
|
||||
<DataFieldFactory
|
||||
withProps={({ data: { raw }, dataExists, onChange, readOnly }) => (
|
||||
<Expandable expandLabel={expander}>
|
||||
<div className="form-group">
|
||||
<label>{translate('TRANS_data')}</label>
|
||||
|
||||
<input
|
||||
className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`}
|
||||
type="text"
|
||||
placeholder={donationAddressMap.ETH}
|
||||
value={raw}
|
||||
readOnly={!!readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</Expandable>
|
||||
<>
|
||||
<label>{translate('OFFLINE_Step2_Label_6')}</label>
|
||||
<input
|
||||
className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`}
|
||||
type="text"
|
||||
placeholder={donationAddressMap.ETH}
|
||||
value={raw}
|
||||
readOnly={!!readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,19 +1,44 @@
|
|||
import React from 'react';
|
||||
import { GasLimitFieldFactory } from './GasLimitFieldFactory';
|
||||
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>
|
||||
<label>{translate('TRANS_gas')} </label>
|
||||
{includeLabel ? <label>{translate('TRANS_gas')} </label> : null}
|
||||
|
||||
<GasLimitFieldFactory
|
||||
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
|
||||
<input
|
||||
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
|
||||
type="text"
|
||||
readOnly={!!readOnly}
|
||||
value={raw}
|
||||
onChange={onChange}
|
||||
/>
|
||||
withProps={({ gasLimit: { raw, value }, onChange, readOnly, gasEstimationPending }) => (
|
||||
<>
|
||||
<GaslimitLoading gasEstimationPending={gasEstimationPending} />
|
||||
{onlyIncludeLoader ? null : (
|
||||
<input
|
||||
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
|
||||
type="number"
|
||||
placeholder="e.g. 21000"
|
||||
readOnly={!!readOnly}
|
||||
value={raw}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -10,6 +10,7 @@ const defaultGasLimit = '21000';
|
|||
export interface CallBackProps {
|
||||
readOnly: boolean;
|
||||
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
||||
gasEstimationPending: boolean;
|
||||
onChange(value: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,14 @@ import React, { Component } from 'react';
|
|||
import { Query } from 'components/renderCbs';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { getGasLimit } from 'selectors/transaction';
|
||||
import { getGasLimit, getGasEstimationPending } from 'selectors/transaction';
|
||||
import { CallBackProps } from 'components/GasLimitFieldFactory';
|
||||
import { getAutoGasLimitEnabled } from 'selectors/config';
|
||||
|
||||
interface StateProps {
|
||||
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
||||
gasEstimationPending: boolean;
|
||||
autoGasLimitEnabled: boolean;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -17,18 +20,24 @@ interface OwnProps {
|
|||
type Props = StateProps & OwnProps;
|
||||
class GasLimitInputClass extends Component<Props> {
|
||||
public render() {
|
||||
const { gasLimit, onChange } = this.props;
|
||||
const { gasLimit, onChange, gasEstimationPending, autoGasLimitEnabled } = this.props;
|
||||
return (
|
||||
<Query
|
||||
params={['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) => ({ gasLimit: getGasLimit(state) }))(
|
||||
GasLimitInputClass
|
||||
);
|
||||
export const GasLimitInput = connect((state: AppState) => ({
|
||||
gasLimit: getGasLimit(state),
|
||||
gasEstimationPending: getGasEstimationPending(state),
|
||||
autoGasLimitEnabled: getAutoGasLimitEnabled(state)
|
||||
}))(GasLimitInputClass);
|
||||
|
|
|
@ -1,37 +1,32 @@
|
|||
import React from 'react';
|
||||
import { translateRaw } from 'translations';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
inputGasPrice,
|
||||
TInputGasPrice,
|
||||
inputGasLimit,
|
||||
TInputGasLimit,
|
||||
inputNonce,
|
||||
TInputNonce
|
||||
} from 'actions/transaction';
|
||||
import { inputGasPrice, TInputGasPrice } from 'actions/transaction';
|
||||
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { getNetworkConfig, getOffline } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import SimpleGas from './components/SimpleGas';
|
||||
import AdvancedGas from './components/AdvancedGas';
|
||||
import './GasSlider.scss';
|
||||
import { getGasPrice } from 'selectors/transaction';
|
||||
|
||||
interface Props {
|
||||
// Component configuration
|
||||
disableAdvanced?: boolean;
|
||||
// Data
|
||||
interface StateProps {
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
||||
nonce: AppState['transaction']['fields']['nonce'];
|
||||
offline: AppState['config']['offline'];
|
||||
network: AppState['config']['network'];
|
||||
// Actions
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
inputGasPrice: TInputGasPrice;
|
||||
inputGasLimit: TInputGasLimit;
|
||||
inputNonce: TInputNonce;
|
||||
fetchCCRates: TFetchCCRates;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
disableAdvanced?: boolean;
|
||||
}
|
||||
|
||||
type Props = DispatchProps & OwnProps & StateProps;
|
||||
|
||||
interface State {
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
@ -54,22 +49,15 @@ class GasSlider extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { gasPrice, gasLimit, nonce, offline, disableAdvanced } = this.props;
|
||||
const { offline, disableAdvanced, gasPrice } = this.props;
|
||||
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
|
||||
|
||||
return (
|
||||
<div className="GasSlider">
|
||||
{showAdvanced ? (
|
||||
<AdvancedGas
|
||||
gasPrice={gasPrice.raw}
|
||||
gasLimit={gasLimit.raw}
|
||||
nonce={nonce.raw}
|
||||
changeGasPrice={this.props.inputGasPrice}
|
||||
changeGasLimit={this.props.inputGasLimit}
|
||||
changeNonce={this.props.inputNonce}
|
||||
/>
|
||||
<AdvancedGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
|
||||
) : (
|
||||
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
|
||||
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
|
||||
)}
|
||||
|
||||
{!offline &&
|
||||
|
@ -79,7 +67,7 @@ class GasSlider extends React.Component<Props, State> {
|
|||
<strong>
|
||||
{showAdvanced
|
||||
? `- ${translateRaw('Back to simple')}`
|
||||
: `+ ${translateRaw('Advanced: Data, Gas Price, Gas Limit')}`}
|
||||
: `+ ${translateRaw('Advanced Settings')}`}
|
||||
</strong>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -93,19 +81,15 @@ class GasSlider extends React.Component<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
function mapStateToProps(state: AppState): StateProps {
|
||||
return {
|
||||
gasPrice: state.transaction.fields.gasPrice,
|
||||
gasLimit: state.transaction.fields.gasLimit,
|
||||
nonce: state.transaction.fields.nonce,
|
||||
offline: state.config.offline,
|
||||
gasPrice: getGasPrice(state),
|
||||
offline: getOffline(state),
|
||||
network: getNetworkConfig(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
inputGasPrice,
|
||||
inputGasLimit,
|
||||
inputNonce,
|
||||
fetchCCRates
|
||||
})(GasSlider);
|
||||
|
|
|
@ -1,4 +1,31 @@
|
|||
.AdvancedGas {
|
||||
margin-top: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,69 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import translate from 'translations';
|
||||
import { DataFieldFactory } from 'components/DataFieldFactory';
|
||||
import FeeSummary from './FeeSummary';
|
||||
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 {
|
||||
gasPrice: string;
|
||||
gasLimit: string;
|
||||
nonce: string;
|
||||
changeGasPrice(gwei: string): void;
|
||||
changeGasLimit(wei: string): void;
|
||||
changeNonce(nonce: string): void;
|
||||
interface OwnProps {
|
||||
inputGasPrice: TInputGasPrice;
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
}
|
||||
|
||||
export default class AdvancedGas extends React.Component<Props> {
|
||||
public render() {
|
||||
// Can't shadow var names for data & fee summary
|
||||
const vals = this.props;
|
||||
interface StateProps {
|
||||
autoGasLimitEnabled: AppState['config']['autoGasLimit'];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
toggleAutoGasLimit: TToggleAutoGasLimit;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class AdvancedGas extends React.Component<Props> {
|
||||
public render() {
|
||||
const { autoGasLimitEnabled, gasPrice } = this.props;
|
||||
return (
|
||||
<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">
|
||||
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
|
||||
<input
|
||||
className={classnames('form-control', !vals.gasPrice && 'is-invalid')}
|
||||
className={classnames('form-control', { 'is-invalid': !gasPrice.value })}
|
||||
type="number"
|
||||
placeholder="e.g. 40"
|
||||
value={vals.gasPrice}
|
||||
value={gasPrice.raw}
|
||||
onChange={this.handleGasPriceChange}
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
className={classnames('form-control', !vals.gasLimit && 'is-invalid')}
|
||||
type="number"
|
||||
placeholder="e.g. 21000"
|
||||
value={vals.gasLimit}
|
||||
onChange={this.handleGasLimitChange}
|
||||
/>
|
||||
<div className="SimpleGas-flex-spacer" />
|
||||
<GasLimitField includeLabel={false} onlyIncludeLoader={false} />
|
||||
</div>
|
||||
|
||||
<div className="col-md-4 col-sm-12">
|
||||
<label>{translate('OFFLINE_Step2_Label_5')}</label>
|
||||
<input
|
||||
className={classnames('form-control', !vals.nonce && 'is-invalid')}
|
||||
type="number"
|
||||
placeholder="e.g. 7"
|
||||
value={vals.nonce}
|
||||
onChange={this.handleNonceChange}
|
||||
/>
|
||||
<NonceField alwaysDisplay={true} />
|
||||
</div>
|
||||
|
||||
<div className="col-md-12">
|
||||
<label>{translate('OFFLINE_Step2_Label_6')}</label>
|
||||
<DataFieldFactory
|
||||
withProps={({ data, onChange }) => (
|
||||
<input
|
||||
className="form-control"
|
||||
value={data.raw}
|
||||
onChange={onChange}
|
||||
placeholder="0x7cB57B5A..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DataField />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-12">
|
||||
|
@ -82,14 +80,15 @@ export default class AdvancedGas extends React.Component<Props> {
|
|||
}
|
||||
|
||||
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.props.changeGasPrice(ev.currentTarget.value);
|
||||
this.props.inputGasPrice(ev.currentTarget.value);
|
||||
};
|
||||
|
||||
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.props.changeGasLimit(ev.currentTarget.value);
|
||||
};
|
||||
|
||||
private handleNonceChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.props.changeNonce(ev.currentTarget.value);
|
||||
private handleToggleAutoGasLimit = (_: React.FormEvent<HTMLInputElement>) => {
|
||||
this.props.toggleAutoGasLimit();
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: AppState) => ({ autoGasLimitEnabled: getAutoGasLimitEnabled(state) }),
|
||||
{ toggleAutoGasLimit }
|
||||
)(AdvancedGas);
|
||||
|
|
|
@ -4,8 +4,24 @@
|
|||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&-label {
|
||||
display: block;
|
||||
&-flex-spacer {
|
||||
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 {
|
||||
|
@ -34,3 +50,26 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade {
|
||||
&-enter,
|
||||
&-exit {
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
|
||||
&-active {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,30 +3,55 @@ import Slider from 'rc-slider';
|
|||
import translate from 'translations';
|
||||
import { gasPriceDefaults } from 'config/data';
|
||||
import FeeSummary from './FeeSummary';
|
||||
import { TInputGasPrice } from 'actions/transaction';
|
||||
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 {
|
||||
gasPrice: string;
|
||||
changeGasPrice(gwei: string): void;
|
||||
interface OwnProps {
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
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() {
|
||||
const { gasPrice } = this.props;
|
||||
const { gasPrice, gasLimitEstimationTimedOut, isWeb3Node } = this.props;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="SimpleGas-flex-spacer" />
|
||||
<GasLimitField includeLabel={false} onlyIncludeLoader={true} />
|
||||
</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="SimpleGas-slider">
|
||||
<Slider
|
||||
onChange={this.handleSlider}
|
||||
min={gasPriceDefaults.gasPriceMinGwei}
|
||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||
value={parseFloat(gasPrice)}
|
||||
value={parseFloat(gasPrice.raw)}
|
||||
/>
|
||||
<div className="SimpleGas-slider-labels">
|
||||
<span>{translate('Cheap')}</span>
|
||||
|
@ -49,6 +74,10 @@ export default class SimpleGas extends React.Component<Props> {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -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);
|
|
@ -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);
|
|
@ -1 +0,0 @@
|
|||
export * from './NonceField';
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1 @@
|
|||
export * from './NonceFieldFactory';
|
|
@ -64,7 +64,7 @@ class DeployClass extends Component<DispatchProps> {
|
|||
</label>
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<NonceField />
|
||||
<NonceField alwaysDisplay={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row form-group">
|
||||
|
|
|
@ -14,7 +14,7 @@ export class Fields extends Component<OwnProps> {
|
|||
<React.Fragment>
|
||||
<GasLimitField />
|
||||
<AmountField />
|
||||
<NonceField />
|
||||
<NonceField alwaysDisplay={false} />
|
||||
{this.props.button}
|
||||
<SigningStatus />
|
||||
<SendButton />
|
||||
|
|
|
@ -106,7 +106,7 @@ class RequestPayment extends React.Component<Props, {}> {
|
|||
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<GasLimitField />
|
||||
<GasLimitField includeLabel={true} onlyIncludeLoader={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -46,10 +46,15 @@ export default class RpcNode implements INode {
|
|||
}
|
||||
|
||||
public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> {
|
||||
// Timeout after 10 seconds
|
||||
|
||||
return this.client
|
||||
.call(this.requests.estimateGas(transaction))
|
||||
.then(isValidEstimateGas)
|
||||
.then(({ result }) => Wei(result));
|
||||
.then(({ result }) => Wei(result))
|
||||
.catch(error => {
|
||||
throw new Error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
public getTokenBalance(
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface State {
|
|||
network: NetworkConfig;
|
||||
isChangingNode: boolean;
|
||||
offline: boolean;
|
||||
autoGasLimit: boolean;
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
latestBlock: string;
|
||||
|
@ -41,6 +42,7 @@ export const INITIAL_STATE: State = {
|
|||
network: NETWORKS[NODES[defaultNode].network],
|
||||
isChangingNode: false,
|
||||
offline: false,
|
||||
autoGasLimit: true,
|
||||
customNodes: [],
|
||||
customNetworks: [],
|
||||
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 {
|
||||
const newId = makeCustomNodeId(action.payload);
|
||||
return {
|
||||
|
@ -132,6 +141,8 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat
|
|||
return changeNodeIntent(state);
|
||||
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
|
||||
return toggleOffline(state);
|
||||
case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT:
|
||||
return toggleAutoGasLimitEstimation(state);
|
||||
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
|
||||
return addCustomNode(state, action);
|
||||
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
|
||||
|
|
|
@ -26,6 +26,8 @@ export const network = (state: State = INITIAL_STATE, action: NetworkAction | Re
|
|||
return nextState('gasEstimationStatus')(state, action);
|
||||
case TK.ESTIMATE_GAS_FAILED:
|
||||
return nextState('gasEstimationStatus')(state, action);
|
||||
case TK.ESTIMATE_GAS_TIMEDOUT:
|
||||
return nextState('gasEstimationStatus')(state, action);
|
||||
case TK.ESTIMATE_GAS_SUCCEEDED:
|
||||
return nextState('gasEstimationStatus')(state, action);
|
||||
case TK.GET_FROM_REQUESTED:
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
export enum RequestStatus {
|
||||
REQUESTED = 'PENDING',
|
||||
SUCCEEDED = 'SUCCESS',
|
||||
FAILED = 'FAIL'
|
||||
FAILED = 'FAIL',
|
||||
TIMEDOUT = 'TIMEDOUT'
|
||||
}
|
||||
export interface State {
|
||||
gasEstimationStatus: RequestStatus | null;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
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 { getNodeLib, getOffline } from 'selectors/config';
|
||||
import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { getTransaction, IGetTransaction } from 'selectors/transaction';
|
||||
import {
|
||||
EstimateGasRequestedAction,
|
||||
setGasLimitField,
|
||||
estimateGasFailed,
|
||||
estimateGasTimedout,
|
||||
estimateGasSucceeded,
|
||||
TypeKeys,
|
||||
estimateGasRequested,
|
||||
|
@ -17,31 +18,36 @@ import {
|
|||
SwapTokenToTokenAction,
|
||||
SwapTokenToEtherAction
|
||||
} from 'actions/transaction';
|
||||
import { TypeKeys as ConfigTypeKeys, ToggleAutoGasLimitAction } from 'actions/config';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction';
|
||||
|
||||
export function* shouldEstimateGas(): SagaIterator {
|
||||
while (true) {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const action:
|
||||
| SetToFieldAction
|
||||
| SetDataFieldAction
|
||||
| SwapEtherToTokenAction
|
||||
| SwapTokenToTokenAction
|
||||
| SwapTokenToEtherAction = yield take([
|
||||
| SwapTokenToEtherAction
|
||||
| ToggleAutoGasLimitAction = yield take([
|
||||
TypeKeys.TO_FIELD_SET,
|
||||
TypeKeys.DATA_FIELD_SET,
|
||||
TypeKeys.ETHER_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
|
||||
// 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 =
|
||||
(action.type === TypeKeys.TO_FIELD_SET || action.type === TypeKeys.DATA_FIELD_SET) &&
|
||||
!action.payload.value &&
|
||||
|
@ -56,6 +62,7 @@ export function* shouldEstimateGas(): SagaIterator {
|
|||
getTransactionFields,
|
||||
transaction
|
||||
);
|
||||
|
||||
yield put(estimateGasRequested(rest));
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +71,10 @@ export function* estimateGas(): SagaIterator {
|
|||
const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1));
|
||||
|
||||
while (true) {
|
||||
const autoGasLimitEnabled: boolean = yield select(getAutoGasLimitEnabled);
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
|
||||
if (isOffline || !autoGasLimitEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -77,17 +86,28 @@ export function* estimateGas(): SagaIterator {
|
|||
try {
|
||||
const from: string = yield apply(walletInst, walletInst.getAddressString);
|
||||
const txObj = { ...payload, from };
|
||||
const gasLimit = yield apply(node, node.estimateGas, [txObj]);
|
||||
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
|
||||
yield put(estimateGasSucceeded());
|
||||
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(estimateGasSucceeded());
|
||||
} else {
|
||||
yield put(estimateGasTimedout());
|
||||
yield call(localGasEstimation, payload);
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(estimateGasFailed());
|
||||
// fallback for estimating locally
|
||||
const tx = yield call(makeTransaction, payload);
|
||||
const gasLimit = yield apply(tx, tx.getBaseFee);
|
||||
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
|
||||
yield call(localGasEstimation, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)];
|
||||
|
|
|
@ -16,6 +16,10 @@ export function getNode(state: AppState): string {
|
|||
return state.config.nodeSelection;
|
||||
}
|
||||
|
||||
export function getIsWeb3Node(state: AppState): boolean {
|
||||
return getNode(state) === 'web3';
|
||||
}
|
||||
|
||||
export function getNodeConfig(state: AppState): NodeConfig {
|
||||
return state.config.node;
|
||||
}
|
||||
|
@ -86,6 +90,10 @@ export function getOffline(state: AppState): boolean {
|
|||
return state.config.offline;
|
||||
}
|
||||
|
||||
export function getAutoGasLimitEnabled(state: AppState): boolean {
|
||||
return state.config.autoGasLimit;
|
||||
}
|
||||
|
||||
export function isSupportedUnit(state: AppState, unit: string) {
|
||||
const isToken: boolean = tokenExists(state, unit);
|
||||
const isEther: boolean = isEtherUnit(unit);
|
||||
|
|
|
@ -2,10 +2,12 @@ import { AppState } from 'reducers';
|
|||
import { getTransactionState } from 'selectors/transaction';
|
||||
import { RequestStatus } from 'reducers/transaction/network';
|
||||
|
||||
const getNetworkStatus = (state: AppState) => getTransactionState(state).network;
|
||||
const nonceRequestFailed = (state: AppState) =>
|
||||
export const getNetworkStatus = (state: AppState) => getTransactionState(state).network;
|
||||
|
||||
export const nonceRequestFailed = (state: AppState) =>
|
||||
getNetworkStatus(state).getNonceStatus === RequestStatus.FAILED;
|
||||
const isNetworkRequestPending = (state: AppState) => {
|
||||
|
||||
export const isNetworkRequestPending = (state: AppState) => {
|
||||
const network = getNetworkStatus(state);
|
||||
const states: RequestStatus[] = Object.values(network);
|
||||
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;
|
||||
|
|
|
@ -133,7 +133,8 @@ const configureStore = () => {
|
|||
nodeSelection: state.config.nodeSelection,
|
||||
languageSelection: state.config.languageSelection,
|
||||
customNodes: state.config.customNodes,
|
||||
customNetworks: state.config.customNetworks
|
||||
customNetworks: state.config.customNetworks,
|
||||
setGasLimit: state.config.setGasLimit
|
||||
},
|
||||
transaction: {
|
||||
fields: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { buffers, delay } from 'redux-saga';
|
||||
import { apply, put, select, take, actionChannel, call } from 'redux-saga/effects';
|
||||
import { getNodeLib, getOffline } from 'selectors/config';
|
||||
import { apply, put, select, take, actionChannel, call, race } from 'redux-saga/effects';
|
||||
import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { getTransaction } from 'selectors/transaction';
|
||||
import {
|
||||
|
@ -8,15 +8,18 @@ import {
|
|||
estimateGasFailed,
|
||||
estimateGasSucceeded,
|
||||
TypeKeys,
|
||||
estimateGasRequested
|
||||
estimateGasRequested,
|
||||
estimateGasTimedout
|
||||
} from 'actions/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 { Wei } from 'libs/units';
|
||||
import { TypeKeys as ConfigTypeKeys } from 'actions/config';
|
||||
|
||||
describe('shouldEstimateGas*', () => {
|
||||
const offline = false;
|
||||
const autoGasLimitEnabled = true;
|
||||
const transaction: any = 'transaction';
|
||||
const tx = { transaction };
|
||||
const rest: any = {
|
||||
|
@ -40,24 +43,29 @@ describe('shouldEstimateGas*', () => {
|
|||
|
||||
const gen = shouldEstimateGas();
|
||||
|
||||
it('should select getOffline', () => {
|
||||
expect(gen.next().value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('should take expected types', () => {
|
||||
expect(gen.next(offline).value).toEqual(
|
||||
expect(gen.next().value).toEqual(
|
||||
take([
|
||||
TypeKeys.TO_FIELD_SET,
|
||||
TypeKeys.DATA_FIELD_SET,
|
||||
TypeKeys.ETHER_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', () => {
|
||||
expect(gen.next(action).value).toEqual(select(getTransaction));
|
||||
expect(gen.next(autoGasLimitEnabled).value).toEqual(select(getTransaction));
|
||||
});
|
||||
|
||||
it('should call getTransactionFields with transaction', () => {
|
||||
|
@ -71,6 +79,7 @@ describe('shouldEstimateGas*', () => {
|
|||
|
||||
describe('estimateGas*', () => {
|
||||
const offline = false;
|
||||
const autoGasLimitEnabled = true;
|
||||
const requestChan = 'requestChan';
|
||||
const payload: any = {
|
||||
mock1: 'mock1',
|
||||
|
@ -86,9 +95,16 @@ describe('estimateGas*', () => {
|
|||
const from = '0xa';
|
||||
const txObj = { ...payload, from };
|
||||
const gasLimit = Wei('100');
|
||||
const successfulGasEstimationResult = {
|
||||
gasLimit
|
||||
};
|
||||
|
||||
const gens: any = {};
|
||||
gens.gen = cloneableGenerator(estimateGas)();
|
||||
const unsuccessfulGasEstimationResult = {
|
||||
gasLimit: null
|
||||
};
|
||||
|
||||
const gens: { [name: string]: any } = {};
|
||||
gens.successCase = cloneableGenerator(estimateGas)();
|
||||
|
||||
let random;
|
||||
beforeAll(() => {
|
||||
|
@ -104,41 +120,53 @@ describe('estimateGas*', () => {
|
|||
const expected = JSON.stringify(
|
||||
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);
|
||||
});
|
||||
|
||||
it('should select autoGasLimit', () => {
|
||||
expect(gens.successCase.next(requestChan).value).toEqual(select(getAutoGasLimitEnabled));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(gens.gen.next(offline).value).toEqual(take(requestChan));
|
||||
expect(gens.successCase.next(offline).value).toEqual(take(requestChan));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(gens.gen.next().value).toEqual(select(getNodeLib));
|
||||
expect(gens.successCase.next().value).toEqual(select(getNodeLib));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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', () => {
|
||||
gens.clone = gens.gen.clone();
|
||||
expect(gens.gen.next(from).value).toEqual(apply(node, node.estimateGas, [txObj]));
|
||||
it('should race between node.estimate gas and a 10 second timeout', () => {
|
||||
gens.failCase = gens.successCase.clone();
|
||||
expect(gens.successCase.next(from).value).toEqual(
|
||||
race({
|
||||
gasLimit: apply(node, node.estimateGas, [txObj]),
|
||||
timeout: call(delay, 10000)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should put setGasLimitField', () => {
|
||||
expect(gens.gen.next(gasLimit).value).toEqual(
|
||||
gens.timeOutCase = gens.successCase.clone();
|
||||
expect(gens.successCase.next(successfulGasEstimationResult).value).toEqual(
|
||||
put(
|
||||
setGasLimitField({
|
||||
raw: gasLimit.toString(),
|
||||
|
@ -149,35 +177,62 @@ describe('estimateGas*', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
const tx = {
|
||||
getBaseFee: jest.fn()
|
||||
};
|
||||
|
||||
it('should catch and put estimateGasFailed', () => {
|
||||
expect(gens.clone.throw().value).toEqual(put(estimateGasFailed()));
|
||||
expect(gens.failCase.throw().value).toEqual(put(estimateGasFailed()));
|
||||
});
|
||||
|
||||
it('should call makeTransaction with payload', () => {
|
||||
expect(gens.clone.next().value).toEqual(call(makeTransaction, payload));
|
||||
});
|
||||
|
||||
it('should apply tx.getBaseFee', () => {
|
||||
expect(gens.clone.next(tx).value).toEqual(apply(tx, tx.getBaseFee));
|
||||
});
|
||||
|
||||
it('should put setGasLimitField', () => {
|
||||
expect(gens.clone.next(gasLimit).value).toEqual(
|
||||
put(
|
||||
setGasLimitField({
|
||||
raw: gasLimit.toString(),
|
||||
value: gasLimit
|
||||
})
|
||||
)
|
||||
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 = {
|
||||
getBaseFee: jest.fn()
|
||||
};
|
||||
const gasLimit = Wei('100');
|
||||
|
||||
const gen = localGasEstimation(payload);
|
||||
it('should call makeTransaction with payload', () => {
|
||||
expect(gen.next().value).toEqual(call(makeTransaction, payload));
|
||||
});
|
||||
|
||||
it('should apply tx.getBaseFee', () => {
|
||||
expect(gen.next(tx).value).toEqual(apply(tx, tx.getBaseFee));
|
||||
});
|
||||
|
||||
it('should put setGasLimitField', () => {
|
||||
expect(gen.next(gasLimit).value).toEqual(
|
||||
put(
|
||||
setGasLimitField({
|
||||
raw: gasLimit.toString(),
|
||||
value: gasLimit
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue