Handle Gas / Estimates on a Per Network Basis (#1160)
* Give each network the ability to specify default estimates, and whether or not they should fetch estimates from API. Convert gas slider to always use estimates. * Fix gas cache invalidation, invalid too high / low logic. * Fix up tests. * tscheck
This commit is contained in:
parent
afaf045edd
commit
c76d0b3fa5
|
@ -17,6 +17,7 @@ export interface GasEstimates {
|
||||||
fast: number;
|
fast: number;
|
||||||
fastest: number;
|
fastest: number;
|
||||||
time: number;
|
time: number;
|
||||||
|
chainId: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +67,7 @@ export function fetchGasEstimates(): Promise<GasEstimates> {
|
||||||
.then((res: RawGasEstimates) => ({
|
.then((res: RawGasEstimates) => ({
|
||||||
...res,
|
...res,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
|
chainId: 1,
|
||||||
isDefault: false
|
isDefault: false
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { gasPriceDefaults, HELP_ARTICLE } from 'config';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DropdownShell, HelpLink } from 'components/ui';
|
|
||||||
import './GasPriceDropdown.scss';
|
|
||||||
import { SetGasLimitFieldAction } from 'actions/transaction';
|
|
||||||
import { gasPricetoBase } from 'libs/units';
|
|
||||||
import { AppState } from 'reducers';
|
|
||||||
import { getGasPrice } from 'selectors/transaction';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
interface OwnProps {
|
|
||||||
onChange(payload: SetGasLimitFieldAction['payload']): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = OwnProps & StateProps;
|
|
||||||
|
|
||||||
class GasPriceDropdown extends Component<Props> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.updateGasPrice = throttle(this.updateGasPrice, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<DropdownShell
|
|
||||||
color="white"
|
|
||||||
size="smr"
|
|
||||||
ariaLabel={`adjust gas price. current price is ${this.props.gasPrice.raw} gwei`}
|
|
||||||
renderLabel={this.renderLabel}
|
|
||||||
renderOptions={this.renderOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderLabel = () => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
Gas Price<span className="hidden-xs">: {this.props.gasPrice.raw} Gwei</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderOptions = () => {
|
|
||||||
return (
|
|
||||||
<div className="GasPrice-dropdown-menu dropdown-menu dropdown-menu-right">
|
|
||||||
<div className="GasPrice-header">
|
|
||||||
<span>Gas Price</span>: {this.props.gasPrice.raw} Gwei
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
value={this.props.gasPrice.raw}
|
|
||||||
min={gasPriceDefaults.minGwei}
|
|
||||||
max={gasPriceDefaults.maxGwei}
|
|
||||||
onChange={this.handleGasPriceChange}
|
|
||||||
/>
|
|
||||||
<p className="small col-xs-4 text-left GasPrice-padding-reset">Not So Fast</p>
|
|
||||||
<p className="small col-xs-4 text-center GasPrice-padding-reset">Fast</p>
|
|
||||||
<p className="small col-xs-4 text-right GasPrice-padding-reset">Fast AF</p>
|
|
||||||
<p className="small GasPrice-description">
|
|
||||||
Gas Price is the amount you pay per unit of gas.{' '}
|
|
||||||
<code>TX fee = gas price * gas limit</code> & is paid to miners for including your TX in
|
|
||||||
a block. Higher the gas price = faster transaction, but more expensive. Default is{' '}
|
|
||||||
<code>21 GWEI</code>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<HelpLink article={HELP_ARTICLE.WHAT_IS_GAS}>Read more</HelpLink>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateGasPrice = (value: string) => {
|
|
||||||
this.props.onChange({ raw: value, value: gasPricetoBase(parseInt(value, 10)) });
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleGasPriceChange = (e: React.FormEvent<HTMLInputElement>) => {
|
|
||||||
this.updateGasPrice(e.currentTarget.value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({ gasPrice: getGasPrice(state) });
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(GasPriceDropdown);
|
|
|
@ -125,8 +125,10 @@ class TXMetaDataPanel extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleGasPriceInput = (raw: string) => {
|
private handleGasPriceInput = (raw: string) => {
|
||||||
const gasBn = new BN(raw);
|
// Realistically, we're not going to end up with a > 32 bit int, so it's
|
||||||
const value = gasBn.mul(new BN(Units.gwei));
|
// safe to cast to float, multiply by gwei units, then big number, since
|
||||||
|
// some of the inputs may be sub-one float values.
|
||||||
|
const value = new BN(parseFloat(raw) * parseFloat(Units.gwei));
|
||||||
this.setState({
|
this.setState({
|
||||||
gasPrice: { raw, value }
|
gasPrice: { raw, value }
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Slider from 'rc-slider';
|
import Slider from 'rc-slider';
|
||||||
import translate, { translateRaw } from 'translations';
|
import translate, { translateRaw } from 'translations';
|
||||||
import { gasPriceDefaults } from 'config';
|
|
||||||
import FeeSummary from './FeeSummary';
|
import FeeSummary from './FeeSummary';
|
||||||
import './SimpleGas.scss';
|
import './SimpleGas.scss';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
|
@ -15,6 +14,7 @@ import { fetchGasEstimates, TFetchGasEstimates } from 'actions/gas';
|
||||||
import { getIsWeb3Node } from 'selectors/config';
|
import { getIsWeb3Node } from 'selectors/config';
|
||||||
import { getEstimates, getIsEstimating } from 'selectors/gas';
|
import { getEstimates, getIsEstimating } from 'selectors/gas';
|
||||||
import { Wei, fromWei } from 'libs/units';
|
import { Wei, fromWei } from 'libs/units';
|
||||||
|
import { gasPriceDefaults } from 'config';
|
||||||
import { InlineSpinner } from 'components/ui/InlineSpinner';
|
import { InlineSpinner } from 'components/ui/InlineSpinner';
|
||||||
const SliderWithTooltip = Slider.createSliderWithTooltip(Slider);
|
const SliderWithTooltip = Slider.createSliderWithTooltip(Slider);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ type Props = OwnProps & StateProps & ActionProps;
|
||||||
|
|
||||||
class SimpleGas extends React.Component<Props> {
|
class SimpleGas extends React.Component<Props> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.fixGasPrice(this.props.gasPrice);
|
this.fixGasPrice();
|
||||||
this.props.fetchGasEstimates();
|
this.props.fetchGasEstimates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,8 +63,8 @@ class SimpleGas extends React.Component<Props> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
max: gasEstimates ? gasEstimates.fastest : gasPriceDefaults.minGwei,
|
max: gasEstimates ? gasEstimates.fastest : gasPriceDefaults.max,
|
||||||
min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.maxGwei
|
min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.min
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,6 +93,7 @@ class SimpleGas extends React.Component<Props> {
|
||||||
onChange={this.handleSlider}
|
onChange={this.handleSlider}
|
||||||
min={bounds.min}
|
min={bounds.min}
|
||||||
max={bounds.max}
|
max={bounds.max}
|
||||||
|
step={bounds.min < 1 ? 0.1 : 1}
|
||||||
value={this.getGasPriceGwei(gasPrice.value)}
|
value={this.getGasPriceGwei(gasPrice.value)}
|
||||||
tipFormatter={this.formatTooltip}
|
tipFormatter={this.formatTooltip}
|
||||||
disabled={isGasEstimating}
|
disabled={isGasEstimating}
|
||||||
|
@ -119,13 +120,18 @@ class SimpleGas extends React.Component<Props> {
|
||||||
this.props.inputGasPrice(gasGwei.toString());
|
this.props.inputGasPrice(gasGwei.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
private fixGasPrice(gasPrice: AppState['transaction']['fields']['gasPrice']) {
|
private fixGasPrice() {
|
||||||
|
const { gasPrice, gasEstimates } = this.props;
|
||||||
|
if (!gasEstimates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If the gas price is above or below our minimum, bring it in line
|
// If the gas price is above or below our minimum, bring it in line
|
||||||
const gasPriceGwei = this.getGasPriceGwei(gasPrice.value);
|
const gasPriceGwei = this.getGasPriceGwei(gasPrice.value);
|
||||||
if (gasPriceGwei > gasPriceDefaults.maxGwei) {
|
if (gasPriceGwei < gasEstimates.safeLow) {
|
||||||
this.props.setGasPrice(gasPriceDefaults.maxGwei.toString());
|
this.props.setGasPrice(gasEstimates.safeLow.toString());
|
||||||
} else if (gasPriceGwei < gasPriceDefaults.minGwei) {
|
} else if (gasPriceGwei > gasEstimates.fastest) {
|
||||||
this.props.setGasPrice(gasPriceDefaults.minGwei.toString());
|
this.props.setGasPrice(gasEstimates.fastest.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,6 @@ export const GAS_LIMIT_LOWER_BOUND = 21000;
|
||||||
export const GAS_LIMIT_UPPER_BOUND = 8000000;
|
export const GAS_LIMIT_UPPER_BOUND = 8000000;
|
||||||
|
|
||||||
// Lower/upper ranges for gas price in gwei
|
// Lower/upper ranges for gas price in gwei
|
||||||
export const GAS_PRICE_GWEI_LOWER_BOUND = 1;
|
export const GAS_PRICE_GWEI_LOWER_BOUND = 0.01;
|
||||||
export const GAS_PRICE_GWEI_UPPER_BOUND = 10000;
|
export const GAS_PRICE_GWEI_UPPER_BOUND = 3000;
|
||||||
export const GAS_PRICE_GWEI_DEFAULT = 40;
|
export const GAS_PRICE_GWEI_DEFAULT = 20;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'; // For ANNOUNCEMENT_MESSAGE jsx
|
import React from 'react'; // For ANNOUNCEMENT_MESSAGE jsx
|
||||||
import { getValues } from '../utils/helpers';
|
import { getValues } from '../utils/helpers';
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
|
import { GasPriceSetting } from 'types/network';
|
||||||
|
|
||||||
export const languages = require('./languages.json');
|
export const languages = require('./languages.json');
|
||||||
|
|
||||||
|
@ -42,12 +43,12 @@ export const donationAddressMap = {
|
||||||
REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520'
|
REP: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gasPriceDefaults = {
|
|
||||||
minGwei: 1,
|
|
||||||
maxGwei: 60,
|
|
||||||
default: 21
|
|
||||||
};
|
|
||||||
export const gasEstimateCacheTime = 60000;
|
export const gasEstimateCacheTime = 60000;
|
||||||
|
export const gasPriceDefaults: GasPriceSetting = {
|
||||||
|
min: 1,
|
||||||
|
max: 60,
|
||||||
|
initial: 20
|
||||||
|
};
|
||||||
|
|
||||||
export const MINIMUM_PASSWORD_LENGTH = 12;
|
export const MINIMUM_PASSWORD_LENGTH = 12;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from 'config/data';
|
import {
|
||||||
|
ethPlorer,
|
||||||
|
ETHTokenExplorer,
|
||||||
|
SecureWalletName,
|
||||||
|
InsecureWalletName,
|
||||||
|
gasPriceDefaults
|
||||||
|
} from 'config/data';
|
||||||
import {
|
import {
|
||||||
ETH_DEFAULT,
|
ETH_DEFAULT,
|
||||||
ETH_TREZOR,
|
ETH_TREZOR,
|
||||||
|
@ -27,7 +33,13 @@ export function makeExplorer(name: string, origin: string): BlockExplorerConfig
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_STATE: State = {
|
const testnetDefaultGasPrice = {
|
||||||
|
min: 0.1,
|
||||||
|
max: 40,
|
||||||
|
initial: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_STATE: State = {
|
||||||
ETH: {
|
ETH: {
|
||||||
name: 'ETH',
|
name: 'ETH',
|
||||||
unit: 'ETH',
|
unit: 'ETH',
|
||||||
|
@ -45,7 +57,9 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: ETH_TREZOR,
|
[SecureWalletName.TREZOR]: ETH_TREZOR,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
|
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
|
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
|
||||||
}
|
},
|
||||||
|
gasPriceSettings: gasPriceDefaults,
|
||||||
|
shouldEstimateGasPrice: true
|
||||||
},
|
},
|
||||||
Ropsten: {
|
Ropsten: {
|
||||||
name: 'Ropsten',
|
name: 'Ropsten',
|
||||||
|
@ -61,7 +75,8 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||||
}
|
},
|
||||||
|
gasPriceSettings: testnetDefaultGasPrice
|
||||||
},
|
},
|
||||||
Kovan: {
|
Kovan: {
|
||||||
name: 'Kovan',
|
name: 'Kovan',
|
||||||
|
@ -77,7 +92,8 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||||
}
|
},
|
||||||
|
gasPriceSettings: testnetDefaultGasPrice
|
||||||
},
|
},
|
||||||
Rinkeby: {
|
Rinkeby: {
|
||||||
name: 'Rinkeby',
|
name: 'Rinkeby',
|
||||||
|
@ -93,7 +109,8 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
||||||
}
|
},
|
||||||
|
gasPriceSettings: testnetDefaultGasPrice
|
||||||
},
|
},
|
||||||
ETC: {
|
ETC: {
|
||||||
name: 'ETC',
|
name: 'ETC',
|
||||||
|
@ -108,6 +125,11 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: ETC_TREZOR,
|
[SecureWalletName.TREZOR]: ETC_TREZOR,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
|
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
|
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
|
||||||
|
},
|
||||||
|
gasPriceSettings: {
|
||||||
|
min: 0.1,
|
||||||
|
max: 10,
|
||||||
|
initial: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
UBQ: {
|
UBQ: {
|
||||||
|
@ -123,6 +145,11 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
|
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
|
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
|
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
|
||||||
|
},
|
||||||
|
gasPriceSettings: {
|
||||||
|
min: 1,
|
||||||
|
max: 60,
|
||||||
|
initial: 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EXP: {
|
EXP: {
|
||||||
|
@ -138,6 +165,11 @@ const INITIAL_STATE: State = {
|
||||||
[SecureWalletName.TREZOR]: EXP_DEFAULT,
|
[SecureWalletName.TREZOR]: EXP_DEFAULT,
|
||||||
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
|
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
|
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
|
||||||
|
},
|
||||||
|
gasPriceSettings: {
|
||||||
|
min: 0.1,
|
||||||
|
max: 20,
|
||||||
|
initial: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
import { Reducer } from 'redux';
|
import { Reducer } from 'redux';
|
||||||
import { State } from './typings';
|
import { State } from './typings';
|
||||||
import { gasPricetoBase } from 'libs/units';
|
import { gasPricetoBase } from 'libs/units';
|
||||||
import { gasPriceDefaults } from 'config';
|
|
||||||
|
|
||||||
const INITIAL_STATE: State = {
|
const INITIAL_STATE: State = {
|
||||||
to: { raw: '', value: null },
|
to: { raw: '', value: null },
|
||||||
|
@ -19,10 +18,7 @@ const INITIAL_STATE: State = {
|
||||||
nonce: { raw: '', value: null },
|
nonce: { raw: '', value: null },
|
||||||
value: { raw: '', value: null },
|
value: { raw: '', value: null },
|
||||||
gasLimit: { raw: '21000', value: new BN(21000) },
|
gasLimit: { raw: '21000', value: new BN(21000) },
|
||||||
gasPrice: {
|
gasPrice: { raw: '20', value: gasPricetoBase(20) }
|
||||||
raw: gasPriceDefaults.default.toString(),
|
|
||||||
value: gasPricetoBase(gasPriceDefaults.default)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
|
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
|
||||||
|
|
|
@ -5,35 +5,49 @@ import { AppState } from 'reducers';
|
||||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||||
import { getEstimates } from 'selectors/gas';
|
import { getEstimates } from 'selectors/gas';
|
||||||
import { getOffline } from 'selectors/config';
|
import { getOffline, getNetworkConfig } from 'selectors/config';
|
||||||
|
import { NetworkConfig } from 'types/network';
|
||||||
|
|
||||||
export function* setDefaultEstimates(): SagaIterator {
|
export function* setDefaultEstimates(network: NetworkConfig): SagaIterator {
|
||||||
// Must yield time for testability
|
// Must yield time for testability
|
||||||
const time = yield call(Date.now);
|
const time = yield call(Date.now);
|
||||||
|
const gasSettings = network.isCustom ? gasPriceDefaults : network.gasPriceSettings;
|
||||||
|
|
||||||
yield put(
|
yield put(
|
||||||
setGasEstimates({
|
setGasEstimates({
|
||||||
safeLow: gasPriceDefaults.minGwei,
|
safeLow: gasSettings.min,
|
||||||
standard: gasPriceDefaults.default,
|
standard: gasSettings.initial,
|
||||||
fast: gasPriceDefaults.default,
|
fast: gasSettings.initial,
|
||||||
fastest: gasPriceDefaults.maxGwei,
|
fastest: gasSettings.max,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
|
chainId: network.chainId,
|
||||||
time
|
time
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* fetchEstimates(): SagaIterator {
|
export function* fetchEstimates(): SagaIterator {
|
||||||
// Don't even try offline
|
// Don't try on non-estimating network
|
||||||
|
const network: NetworkConfig = yield select(getNetworkConfig);
|
||||||
|
if (network.isCustom || !network.shouldEstimateGasPrice) {
|
||||||
|
yield call(setDefaultEstimates, network);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't try while offline
|
||||||
const isOffline: boolean = yield select(getOffline);
|
const isOffline: boolean = yield select(getOffline);
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
yield call(setDefaultEstimates);
|
yield call(setDefaultEstimates, network);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache estimates for a bit
|
// Cache estimates for a bit
|
||||||
const oldEstimates: AppState['gas']['estimates'] = yield select(getEstimates);
|
const oldEstimates: AppState['gas']['estimates'] = yield select(getEstimates);
|
||||||
if (oldEstimates && oldEstimates.time + gasEstimateCacheTime > Date.now()) {
|
if (
|
||||||
|
oldEstimates &&
|
||||||
|
oldEstimates.chainId === network.chainId &&
|
||||||
|
oldEstimates.time + gasEstimateCacheTime > Date.now()
|
||||||
|
) {
|
||||||
yield put(setGasEstimates(oldEstimates));
|
yield put(setGasEstimates(oldEstimates));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +58,7 @@ export function* fetchEstimates(): SagaIterator {
|
||||||
yield put(setGasEstimates(estimates));
|
yield put(setGasEstimates(estimates));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to fetch gas estimates:', err);
|
console.warn('Failed to fetch gas estimates:', err);
|
||||||
yield call(setDefaultEstimates);
|
yield call(setDefaultEstimates, network);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,12 @@ interface DPathFormats {
|
||||||
mnemonicPhrase: DPath;
|
mnemonicPhrase: DPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GasPriceSetting {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
initial: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface StaticNetworkConfig {
|
interface StaticNetworkConfig {
|
||||||
isCustom: false; // used for type guards
|
isCustom: false; // used for type guards
|
||||||
name: StaticNetworkIds;
|
name: StaticNetworkIds;
|
||||||
|
@ -44,6 +50,8 @@ interface StaticNetworkConfig {
|
||||||
contracts: NetworkContract[] | null;
|
contracts: NetworkContract[] | null;
|
||||||
dPathFormats: DPathFormats;
|
dPathFormats: DPathFormats;
|
||||||
isTestnet?: boolean;
|
isTestnet?: boolean;
|
||||||
|
gasPriceSettings: GasPriceSetting;
|
||||||
|
shouldEstimateGasPrice?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomNetworkConfig {
|
interface CustomNetworkConfig {
|
||||||
|
|
|
@ -1,138 +1,8 @@
|
||||||
import { staticNetworks, makeExplorer } from 'reducers/config/networks/staticNetworks';
|
import { INITIAL_STATE, staticNetworks } from 'reducers/config/networks/staticNetworks';
|
||||||
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from 'config/data';
|
|
||||||
import {
|
|
||||||
ETH_DEFAULT,
|
|
||||||
ETH_TREZOR,
|
|
||||||
ETH_LEDGER,
|
|
||||||
ETC_LEDGER,
|
|
||||||
ETC_TREZOR,
|
|
||||||
ETH_TESTNET,
|
|
||||||
EXP_DEFAULT,
|
|
||||||
UBQ_DEFAULT
|
|
||||||
} from 'config/dpaths';
|
|
||||||
|
|
||||||
const expectedInitialState = {
|
|
||||||
ETH: {
|
|
||||||
name: 'ETH',
|
|
||||||
unit: 'ETH',
|
|
||||||
chainId: 1,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#0e97c0',
|
|
||||||
blockExplorer: makeExplorer('Etherscan', 'https://etherscan.io'),
|
|
||||||
tokenExplorer: {
|
|
||||||
name: ethPlorer,
|
|
||||||
address: ETHTokenExplorer
|
|
||||||
},
|
|
||||||
tokens: require('config/tokens/eth.json'),
|
|
||||||
contracts: require('config/contracts/eth.json'),
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: ETH_TREZOR,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ropsten: {
|
|
||||||
name: 'Ropsten',
|
|
||||||
unit: 'ETH',
|
|
||||||
chainId: 3,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#adc101',
|
|
||||||
blockExplorer: makeExplorer('Etherscan', 'https://ropsten.etherscan.io'),
|
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
|
||||||
isTestnet: true,
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Kovan: {
|
|
||||||
name: 'Kovan',
|
|
||||||
unit: 'ETH',
|
|
||||||
chainId: 42,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#adc101',
|
|
||||||
blockExplorer: makeExplorer('Etherscan', 'https://kovan.etherscan.io'),
|
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
|
||||||
isTestnet: true,
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Rinkeby: {
|
|
||||||
name: 'Rinkeby',
|
|
||||||
unit: 'ETH',
|
|
||||||
chainId: 4,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#adc101',
|
|
||||||
blockExplorer: makeExplorer('Etherscan', 'https://rinkeby.etherscan.io'),
|
|
||||||
tokens: require('config/tokens/rinkeby.json'),
|
|
||||||
contracts: require('config/contracts/rinkeby.json'),
|
|
||||||
isTestnet: true,
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: ETH_TESTNET,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ETC: {
|
|
||||||
name: 'ETC',
|
|
||||||
unit: 'ETC',
|
|
||||||
chainId: 61,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#669073',
|
|
||||||
blockExplorer: makeExplorer('GasTracker', 'https://gastracker.io'),
|
|
||||||
tokens: require('config/tokens/etc.json'),
|
|
||||||
contracts: require('config/contracts/etc.json'),
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: ETC_TREZOR,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UBQ: {
|
|
||||||
name: 'UBQ',
|
|
||||||
unit: 'UBQ',
|
|
||||||
chainId: 8,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#b37aff',
|
|
||||||
blockExplorer: makeExplorer('Ubiqscan', 'https://ubiqscan.io/en'),
|
|
||||||
tokens: require('config/tokens/ubq.json'),
|
|
||||||
contracts: require('config/contracts/ubq.json'),
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EXP: {
|
|
||||||
name: 'EXP',
|
|
||||||
unit: 'EXP',
|
|
||||||
chainId: 2,
|
|
||||||
isCustom: false,
|
|
||||||
color: '#673ab7',
|
|
||||||
blockExplorer: makeExplorer('Gander', 'https://www.gander.tech'),
|
|
||||||
tokens: require('config/tokens/exp.json'),
|
|
||||||
contracts: require('config/contracts/exp.json'),
|
|
||||||
dPathFormats: {
|
|
||||||
[SecureWalletName.TREZOR]: EXP_DEFAULT,
|
|
||||||
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
|
|
||||||
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedState = {
|
|
||||||
initialState: expectedInitialState
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Testing contained data', () => {
|
describe('Testing contained data', () => {
|
||||||
it(`contain unique chainIds`, () => {
|
it(`contain unique chainIds`, () => {
|
||||||
const networkValues = Object.values(expectedInitialState);
|
const networkValues = Object.values(INITIAL_STATE);
|
||||||
const chainIds = networkValues.map(a => a.chainId);
|
const chainIds = networkValues.map(a => a.chainId);
|
||||||
const chainIdsSet = new Set(chainIds);
|
const chainIdsSet = new Set(chainIds);
|
||||||
expect(Array.from(chainIdsSet).length).toEqual(chainIds.length);
|
expect(Array.from(chainIdsSet).length).toEqual(chainIds.length);
|
||||||
|
@ -142,8 +12,6 @@ describe('Testing contained data', () => {
|
||||||
describe('static networks reducer', () => {
|
describe('static networks reducer', () => {
|
||||||
it('should return the initial state', () =>
|
it('should return the initial state', () =>
|
||||||
expect(JSON.stringify(staticNetworks(undefined, {} as any))).toEqual(
|
expect(JSON.stringify(staticNetworks(undefined, {} as any))).toEqual(
|
||||||
JSON.stringify(expectedState.initialState)
|
JSON.stringify(INITIAL_STATE)
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
export { expectedState as staticNetworksExpectedState };
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ describe('gas reducer', () => {
|
||||||
fast: 4,
|
fast: 4,
|
||||||
fastest: 20,
|
fastest: 20,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
|
chainId: 1,
|
||||||
isDefault: false
|
isDefault: false
|
||||||
};
|
};
|
||||||
const state = gas(undefined, setGasEstimates(estimates));
|
const state = gas(undefined, setGasEstimates(estimates));
|
||||||
|
|
|
@ -4,8 +4,13 @@ import { cloneableGenerator } from 'redux-saga/utils';
|
||||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||||
import { setGasEstimates } from 'actions/gas';
|
import { setGasEstimates } from 'actions/gas';
|
||||||
import { getEstimates } from 'selectors/gas';
|
import { getEstimates } from 'selectors/gas';
|
||||||
import { getOffline } from 'selectors/config';
|
import { getOffline, getNetworkConfig } from 'selectors/config';
|
||||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||||
|
import { staticNetworks } from 'reducers/config/networks/staticNetworks';
|
||||||
|
|
||||||
|
const networkState = staticNetworks(undefined, {} as any);
|
||||||
|
const network = networkState.ETH;
|
||||||
|
const nonEstimateNetwork = networkState.ETC;
|
||||||
|
|
||||||
describe('fetchEstimates*', () => {
|
describe('fetchEstimates*', () => {
|
||||||
const gen = cloneableGenerator(fetchEstimates)();
|
const gen = cloneableGenerator(fetchEstimates)();
|
||||||
|
@ -16,24 +21,42 @@ describe('fetchEstimates*', () => {
|
||||||
fast: 4,
|
fast: 4,
|
||||||
fastest: 20,
|
fastest: 20,
|
||||||
time: Date.now() - gasEstimateCacheTime - 1000,
|
time: Date.now() - gasEstimateCacheTime - 1000,
|
||||||
|
chainId: network.chainId,
|
||||||
isDefault: false
|
isDefault: false
|
||||||
};
|
};
|
||||||
const newEstimates: GasEstimates = {
|
const newTimeEstimates: GasEstimates = {
|
||||||
safeLow: 2,
|
safeLow: 2,
|
||||||
standard: 2,
|
standard: 2,
|
||||||
fast: 8,
|
fast: 8,
|
||||||
fastest: 80,
|
fastest: 80,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
|
chainId: network.chainId,
|
||||||
isDefault: false
|
isDefault: false
|
||||||
};
|
};
|
||||||
|
const newChainIdEstimates: GasEstimates = {
|
||||||
|
...oldEstimates,
|
||||||
|
chainId: network.chainId + 1
|
||||||
|
};
|
||||||
|
|
||||||
it('Should select getOffline', () => {
|
it('Should select getNetworkConfig', () => {
|
||||||
expect(gen.next().value).toEqual(select(getOffline));
|
expect(gen.next().value).toEqual(select(getNetworkConfig));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should use default estimates if offline', () => {
|
it('Should use network default gas price settings if network shouldn’t estimate', () => {
|
||||||
|
const noEstimateGen = gen.clone();
|
||||||
|
expect(noEstimateGen.next(nonEstimateNetwork).value).toEqual(
|
||||||
|
call(setDefaultEstimates, nonEstimateNetwork)
|
||||||
|
);
|
||||||
|
expect(noEstimateGen.next().done).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should select getOffline', () => {
|
||||||
|
expect(gen.next(network).value).toEqual(select(getOffline));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use network default gas price settings if offline', () => {
|
||||||
const offlineGen = gen.clone();
|
const offlineGen = gen.clone();
|
||||||
expect(offlineGen.next(true).value).toEqual(call(setDefaultEstimates));
|
expect(offlineGen.next(true).value).toEqual(call(setDefaultEstimates, network));
|
||||||
expect(offlineGen.next().done).toBeTruthy();
|
expect(offlineGen.next().done).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,32 +82,67 @@ describe('fetchEstimates*', () => {
|
||||||
const failedReqGen = gen.clone();
|
const failedReqGen = gen.clone();
|
||||||
// Not sure why, but typescript seems to think throw might be missing.
|
// Not sure why, but typescript seems to think throw might be missing.
|
||||||
if (failedReqGen.throw) {
|
if (failedReqGen.throw) {
|
||||||
expect(failedReqGen.throw('test').value).toEqual(call(setDefaultEstimates));
|
expect(failedReqGen.throw('test').value).toEqual(call(setDefaultEstimates, network));
|
||||||
expect(failedReqGen.next().done).toBeTruthy();
|
expect(failedReqGen.next().done).toBeTruthy();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('SagaIterator didn’t have throw');
|
throw new Error('SagaIterator didn’t have throw');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should use new estimates if chainId changed, even if time is similar', () => {
|
||||||
|
const newChainGen = gen.clone();
|
||||||
|
expect(newChainGen.next(newChainIdEstimates).value).toEqual(
|
||||||
|
put(setGasEstimates(newChainIdEstimates))
|
||||||
|
);
|
||||||
|
expect(newChainGen.next().done).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it('Should use fetched estimates', () => {
|
it('Should use fetched estimates', () => {
|
||||||
expect(gen.next(newEstimates).value).toEqual(put(setGasEstimates(newEstimates)));
|
expect(gen.next(newTimeEstimates).value).toEqual(put(setGasEstimates(newTimeEstimates)));
|
||||||
expect(gen.next().done).toBeTruthy();
|
expect(gen.next().done).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setDefaultEstimates*', () => {
|
describe('setDefaultEstimates*', () => {
|
||||||
const gen = cloneableGenerator(setDefaultEstimates)();
|
const time = Date.now();
|
||||||
|
|
||||||
it('Should put setGasEstimates with config defaults', () => {
|
it('Should put setGasEstimates with config defaults', () => {
|
||||||
const time = Date.now();
|
const gen = setDefaultEstimates(network);
|
||||||
gen.next();
|
gen.next();
|
||||||
expect(gen.next(time).value).toEqual(
|
expect(gen.next(time).value).toEqual(
|
||||||
put(
|
put(
|
||||||
setGasEstimates({
|
setGasEstimates({
|
||||||
safeLow: gasPriceDefaults.minGwei,
|
safeLow: network.gasPriceSettings.min,
|
||||||
standard: gasPriceDefaults.default,
|
standard: network.gasPriceSettings.initial,
|
||||||
fast: gasPriceDefaults.default,
|
fast: network.gasPriceSettings.initial,
|
||||||
fastest: gasPriceDefaults.maxGwei,
|
fastest: network.gasPriceSettings.max,
|
||||||
|
chainId: network.chainId,
|
||||||
|
isDefault: true,
|
||||||
|
time
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use config defaults if network has no defaults', () => {
|
||||||
|
const customNetwork = {
|
||||||
|
isCustom: true as true,
|
||||||
|
name: 'Custon',
|
||||||
|
unit: 'CST',
|
||||||
|
chainId: 123,
|
||||||
|
dPathFormats: null
|
||||||
|
};
|
||||||
|
const gen = setDefaultEstimates(customNetwork);
|
||||||
|
|
||||||
|
gen.next();
|
||||||
|
expect(gen.next(time).value).toEqual(
|
||||||
|
put(
|
||||||
|
setGasEstimates({
|
||||||
|
safeLow: gasPriceDefaults.min,
|
||||||
|
standard: gasPriceDefaults.initial,
|
||||||
|
fast: gasPriceDefaults.initial,
|
||||||
|
fastest: gasPriceDefaults.max,
|
||||||
|
chainId: customNetwork.chainId,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
time
|
time
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue