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:
William O'Beirne 2018-02-24 13:00:00 -05:00 committed by Daniel Ternyak
parent afaf045edd
commit c76d0b3fa5
13 changed files with 177 additions and 277 deletions

View File

@ -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
})); }));
} }

View File

@ -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);

View File

@ -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 }
}); });

View File

@ -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());
} }
} }

View File

@ -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;

View File

@ -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;

View File

@ -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
} }
} }
}; };

View File

@ -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) => ({

View File

@ -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);
} }
} }

View File

@ -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 {

View File

@ -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 };

View File

@ -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));

View File

@ -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 shouldnt 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 didnt have throw'); throw new Error('SagaIterator didnt 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
}) })