Gas Price Estimates API (#1050)
* Setup api / reducers / actions for gas. * Implement gas price saga, fetch from component, and loading states. Blocked on CORS. * Implement caching mechanism. * Add tests for gas saga and reducer. * More testing. * Indicate that gas price is recommended when fetched from API. * Hide track while loading. * Fix tscheck. * Check gas estimate before assuming its ok. * Check for correct logical order of gas prices. * Tscheck fixes.
This commit is contained in:
parent
cec0d690c7
commit
31912c0f83
|
@ -0,0 +1,19 @@
|
|||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
export type TFetchGasEstimates = typeof fetchGasEstimates;
|
||||
export function fetchGasEstimates(): interfaces.FetchGasEstimatesAction {
|
||||
return {
|
||||
type: TypeKeys.GAS_FETCH_ESTIMATES
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetGasEstimates = typeof setGasEstimates;
|
||||
export function setGasEstimates(
|
||||
payload: interfaces.SetGasEstimatesAction['payload']
|
||||
): interfaces.SetGasEstimatesAction {
|
||||
return {
|
||||
type: TypeKeys.GAS_SET_ESTIMATES,
|
||||
payload
|
||||
};
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
export interface FetchGasEstimatesAction {
|
||||
type: TypeKeys.GAS_FETCH_ESTIMATES;
|
||||
}
|
||||
|
||||
export interface SetGasEstimatesAction {
|
||||
type: TypeKeys.GAS_SET_ESTIMATES;
|
||||
payload: GasEstimates;
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type GasAction = FetchGasEstimatesAction | SetGasEstimatesAction;
|
|
@ -0,0 +1,4 @@
|
|||
export enum TypeKeys {
|
||||
GAS_FETCH_ESTIMATES = 'GAS_FETCH_ESTIMATES',
|
||||
GAS_SET_ESTIMATES = 'GAS_SET_ESTIMATES'
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './actionCreators';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
|
@ -0,0 +1,71 @@
|
|||
import { checkHttpStatus, parseJSON } from './utils';
|
||||
|
||||
const MAX_GAS_FAST = 250;
|
||||
|
||||
interface RawGasEstimates {
|
||||
safeLow: number;
|
||||
standard: number;
|
||||
fast: number;
|
||||
fastest: number;
|
||||
block_time: number;
|
||||
blockNum: number;
|
||||
}
|
||||
|
||||
export interface GasEstimates {
|
||||
safeLow: number;
|
||||
standard: number;
|
||||
fast: number;
|
||||
fastest: number;
|
||||
time: number;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export function fetchGasEstimates(): Promise<GasEstimates> {
|
||||
return fetch('https://dev.blockscale.net/api/gasexpress.json', {
|
||||
mode: 'cors'
|
||||
})
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON)
|
||||
.then((res: object) => {
|
||||
// Make sure it looks like a raw gas estimate, and it has valid values
|
||||
const keys = ['safeLow', 'standard', 'fast', 'fastest'];
|
||||
keys.forEach(key => {
|
||||
if (typeof res[key] !== 'number') {
|
||||
throw new Error(
|
||||
`Gas estimate API has invalid shape: Expected numeric key '${key}' in response, got '${
|
||||
res[key]
|
||||
}' instead`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the estimate isn't totally crazy
|
||||
const estimateRes = res as RawGasEstimates;
|
||||
if (estimateRes.fast > MAX_GAS_FAST) {
|
||||
throw new Error(
|
||||
`Gas estimate response estimate too high: Max fast is ${MAX_GAS_FAST}, was given ${
|
||||
estimateRes.fast
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
estimateRes.safeLow > estimateRes.standard ||
|
||||
estimateRes.standard > estimateRes.fast ||
|
||||
estimateRes.fast > estimateRes.fastest
|
||||
) {
|
||||
throw new Error(
|
||||
`Gas esimates are in illogical order: should be safeLow < standard < fast < fastest, received ${
|
||||
estimateRes.safeLow
|
||||
} < ${estimateRes.standard} < ${estimateRes.fast} < ${estimateRes.fastest}`
|
||||
);
|
||||
}
|
||||
|
||||
return estimateRes;
|
||||
})
|
||||
.then((res: RawGasEstimates) => ({
|
||||
...res,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
}));
|
||||
}
|
|
@ -53,8 +53,8 @@ class GasPriceDropdown extends Component<Props> {
|
|||
<input
|
||||
type="range"
|
||||
value={this.props.gasPrice.raw}
|
||||
min={gasPriceDefaults.gasPriceMinGwei}
|
||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||
min={gasPriceDefaults.minGwei}
|
||||
max={gasPriceDefaults.maxGwei}
|
||||
onChange={this.handleGasPriceChange}
|
||||
/>
|
||||
<p className="small col-xs-4 text-left GasPrice-padding-reset">Not So Fast</p>
|
||||
|
|
|
@ -3,7 +3,9 @@ import BN from 'bn.js';
|
|||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { getNetworkConfig, getOffline } from 'selectors/config';
|
||||
import { UnitDisplay } from 'components/ui';
|
||||
import { getIsEstimating } from 'selectors/gas';
|
||||
import { getGasLimit } from 'selectors/transaction';
|
||||
import { UnitDisplay, Spinner } from 'components/ui';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import './FeeSummary.scss';
|
||||
|
||||
|
@ -20,6 +22,7 @@ interface ReduxStateProps {
|
|||
rates: AppState['rates']['rates'];
|
||||
network: NetworkConfig;
|
||||
isOffline: AppState['config']['meta']['offline'];
|
||||
isGasEstimating: AppState['gas']['isEstimating'];
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -31,7 +34,15 @@ type Props = OwnProps & ReduxStateProps;
|
|||
|
||||
class FeeSummary extends React.Component<Props> {
|
||||
public render() {
|
||||
const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
|
||||
const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props;
|
||||
|
||||
if (isGasEstimating) {
|
||||
return (
|
||||
<div className="FeeSummary is-loading">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
|
||||
const fee = (
|
||||
|
@ -73,10 +84,11 @@ class FeeSummary extends React.Component<Props> {
|
|||
|
||||
function mapStateToProps(state: AppState): ReduxStateProps {
|
||||
return {
|
||||
gasLimit: state.transaction.fields.gasLimit,
|
||||
gasLimit: getGasLimit(state),
|
||||
rates: state.rates.rates,
|
||||
network: getNetworkConfig(state),
|
||||
isOffline: getOffline(state)
|
||||
isOffline: getOffline(state),
|
||||
isGasEstimating: getIsEstimating(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,33 +11,50 @@ import {
|
|||
nonceRequestPending
|
||||
} from 'selectors/transaction';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchGasEstimates, TFetchGasEstimates } from 'actions/gas';
|
||||
import { getIsWeb3Node } from 'selectors/config';
|
||||
import { getEstimates, getIsEstimating } from 'selectors/gas';
|
||||
import { Wei, fromWei } from 'libs/units';
|
||||
import { InlineSpinner } from 'components/ui/InlineSpinner';
|
||||
const SliderWithTooltip = Slider.createSliderWithTooltip(Slider);
|
||||
|
||||
interface OwnProps {
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
noncePending: boolean;
|
||||
gasLimitPending: boolean;
|
||||
inputGasPrice(rawGas: string);
|
||||
setGasPrice(rawGas: string);
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
gasEstimates: AppState['gas']['estimates'];
|
||||
isGasEstimating: AppState['gas']['isEstimating'];
|
||||
noncePending: boolean;
|
||||
gasLimitPending: boolean;
|
||||
isWeb3Node: boolean;
|
||||
gasLimitEstimationTimedOut: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
interface ActionProps {
|
||||
fetchGasEstimates: TFetchGasEstimates;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & ActionProps;
|
||||
|
||||
class SimpleGas extends React.Component<Props> {
|
||||
public componentDidMount() {
|
||||
this.fixGasPrice(this.props.gasPrice);
|
||||
this.props.fetchGasEstimates();
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (!this.props.gasEstimates && nextProps.gasEstimates) {
|
||||
this.props.setGasPrice(nextProps.gasEstimates.fast.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isGasEstimating,
|
||||
gasEstimates,
|
||||
gasPrice,
|
||||
gasLimitEstimationTimedOut,
|
||||
isWeb3Node,
|
||||
|
@ -45,6 +62,11 @@ class SimpleGas extends React.Component<Props> {
|
|||
gasLimitPending
|
||||
} = this.props;
|
||||
|
||||
const bounds = {
|
||||
max: gasEstimates ? gasEstimates.fastest : gasPriceDefaults.minGwei,
|
||||
min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.maxGwei
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="SimpleGas row form-group">
|
||||
<div className="SimpleGas-title">
|
||||
|
@ -69,14 +91,14 @@ class SimpleGas extends React.Component<Props> {
|
|||
<div className="SimpleGas-slider">
|
||||
<SliderWithTooltip
|
||||
onChange={this.handleSlider}
|
||||
min={gasPriceDefaults.gasPriceMinGwei}
|
||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||
min={bounds.min}
|
||||
max={bounds.max}
|
||||
value={this.getGasPriceGwei(gasPrice.value)}
|
||||
tipFormatter={gas => `${gas} Gwei`}
|
||||
tipFormatter={this.formatTooltip}
|
||||
disabled={isGasEstimating}
|
||||
/>
|
||||
<div className="SimpleGas-slider-labels">
|
||||
<span>{translate('Cheap')}</span>
|
||||
<span>{translate('Balanced')}</span>
|
||||
<span>{translate('Fast')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -100,21 +122,38 @@ class SimpleGas extends React.Component<Props> {
|
|||
private fixGasPrice(gasPrice: AppState['transaction']['fields']['gasPrice']) {
|
||||
// If the gas price is above or below our minimum, bring it in line
|
||||
const gasPriceGwei = this.getGasPriceGwei(gasPrice.value);
|
||||
if (gasPriceGwei > gasPriceDefaults.gasPriceMaxGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.gasPriceMaxGwei.toString());
|
||||
} else if (gasPriceGwei < gasPriceDefaults.gasPriceMinGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.gasPriceMinGwei.toString());
|
||||
if (gasPriceGwei > gasPriceDefaults.maxGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.maxGwei.toString());
|
||||
} else if (gasPriceGwei < gasPriceDefaults.minGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.minGwei.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private getGasPriceGwei(gasPriceValue: Wei) {
|
||||
return parseFloat(fromWei(gasPriceValue, 'gwei'));
|
||||
}
|
||||
|
||||
private formatTooltip = (gas: number) => {
|
||||
const { gasEstimates } = this.props;
|
||||
let recommended = '';
|
||||
if (gasEstimates && !gasEstimates.isDefault && gas === gasEstimates.fast) {
|
||||
recommended = '(Recommended)';
|
||||
}
|
||||
|
||||
return `${gas} Gwei ${recommended}`;
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
noncePending: nonceRequestPending(state),
|
||||
gasLimitPending: getGasEstimationPending(state),
|
||||
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
|
||||
isWeb3Node: getIsWeb3Node(state)
|
||||
}))(SimpleGas);
|
||||
export default connect(
|
||||
(state: AppState): StateProps => ({
|
||||
gasEstimates: getEstimates(state),
|
||||
isGasEstimating: getIsEstimating(state),
|
||||
noncePending: nonceRequestPending(state),
|
||||
gasLimitPending: getGasEstimationPending(state),
|
||||
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
|
||||
isWeb3Node: getIsWeb3Node(state)
|
||||
}),
|
||||
{
|
||||
fetchGasEstimates
|
||||
}
|
||||
)(SimpleGas);
|
||||
|
|
|
@ -5,3 +5,4 @@ export const GAS_LIMIT_UPPER_BOUND = 8000000;
|
|||
// Lower/upper ranges for gas price in gwei
|
||||
export const GAS_PRICE_GWEI_LOWER_BOUND = 1;
|
||||
export const GAS_PRICE_GWEI_UPPER_BOUND = 10000;
|
||||
export const GAS_PRICE_GWEI_DEFAULT = 40;
|
||||
|
|
|
@ -42,9 +42,11 @@ export const donationAddressMap = {
|
|||
};
|
||||
|
||||
export const gasPriceDefaults = {
|
||||
gasPriceMinGwei: 1,
|
||||
gasPriceMaxGwei: 60
|
||||
minGwei: 1,
|
||||
maxGwei: 60,
|
||||
default: 21
|
||||
};
|
||||
export const gasEstimateCacheTime = 60000;
|
||||
|
||||
export const MINIMUM_PASSWORD_LENGTH = 12;
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { SetGasEstimatesAction, GasAction, TypeKeys } from 'actions/gas';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
export interface State {
|
||||
estimates: GasEstimates | null;
|
||||
isEstimating: boolean;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
estimates: null,
|
||||
isEstimating: false
|
||||
};
|
||||
|
||||
function fetchGasEstimates(state: State): State {
|
||||
return {
|
||||
...state,
|
||||
isEstimating: true
|
||||
};
|
||||
}
|
||||
|
||||
function setGasEstimates(state: State, action: SetGasEstimatesAction): State {
|
||||
return {
|
||||
...state,
|
||||
estimates: action.payload,
|
||||
isEstimating: false
|
||||
};
|
||||
}
|
||||
|
||||
export function gas(state: State = INITIAL_STATE, action: GasAction): State {
|
||||
switch (action.type) {
|
||||
case TypeKeys.GAS_FETCH_ESTIMATES:
|
||||
return fetchGasEstimates(state);
|
||||
case TypeKeys.GAS_SET_ESTIMATES:
|
||||
return setGasEstimates(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import { rates, State as RatesState } from './rates';
|
|||
import { State as SwapState, swap } from './swap';
|
||||
import { State as WalletState, wallet } from './wallet';
|
||||
import { State as TransactionState, transaction } from './transaction';
|
||||
import { State as GasState, gas } from './gas';
|
||||
import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
|
||||
import { State as TransactionsState, transactions } from './transactions';
|
||||
|
||||
|
@ -25,6 +26,7 @@ export interface AppState {
|
|||
swap: SwapState;
|
||||
transaction: TransactionState;
|
||||
transactions: TransactionsState;
|
||||
gas: GasState;
|
||||
// Third party reducers (TODO: Fill these out)
|
||||
routing: any;
|
||||
}
|
||||
|
@ -41,5 +43,6 @@ export default combineReducers<AppState>({
|
|||
deterministicWallets,
|
||||
transaction,
|
||||
transactions,
|
||||
gas,
|
||||
routing: routerReducer
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Reducer } from 'redux';
|
||||
import { State } from './typings';
|
||||
import { gasPricetoBase } from 'libs/units';
|
||||
import { gasPriceDefaults } from 'config';
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
to: { raw: '', value: null },
|
||||
|
@ -18,7 +19,10 @@ const INITIAL_STATE: State = {
|
|||
nonce: { raw: '', value: null },
|
||||
value: { raw: '', value: null },
|
||||
gasLimit: { raw: '21000', value: new BN(21000) },
|
||||
gasPrice: { raw: '21', value: gasPricetoBase(21) }
|
||||
gasPrice: {
|
||||
raw: gasPriceDefaults.default.toString(),
|
||||
value: gasPricetoBase(gasPriceDefaults.default)
|
||||
}
|
||||
};
|
||||
|
||||
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { setGasEstimates, TypeKeys } from 'actions/gas';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import { AppState } from 'reducers';
|
||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||
import { getEstimates } from 'selectors/gas';
|
||||
import { getOffline } from 'selectors/config';
|
||||
|
||||
export function* setDefaultEstimates(): SagaIterator {
|
||||
// Must yield time for testability
|
||||
const time = yield call(Date.now);
|
||||
|
||||
yield put(
|
||||
setGasEstimates({
|
||||
safeLow: gasPriceDefaults.minGwei,
|
||||
standard: gasPriceDefaults.default,
|
||||
fast: gasPriceDefaults.default,
|
||||
fastest: gasPriceDefaults.maxGwei,
|
||||
isDefault: true,
|
||||
time
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function* fetchEstimates(): SagaIterator {
|
||||
// Don't even try offline
|
||||
const isOffline: boolean = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
yield call(setDefaultEstimates);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache estimates for a bit
|
||||
const oldEstimates: AppState['gas']['estimates'] = yield select(getEstimates);
|
||||
if (oldEstimates && oldEstimates.time + gasEstimateCacheTime > Date.now()) {
|
||||
yield put(setGasEstimates(oldEstimates));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fetch new estimates
|
||||
try {
|
||||
const estimates: GasEstimates = yield call(fetchGasEstimates);
|
||||
yield put(setGasEstimates(estimates));
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch gas estimates:', err);
|
||||
yield call(setDefaultEstimates);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* gas(): SagaIterator {
|
||||
yield takeLatest(TypeKeys.GAS_FETCH_ESTIMATES, fetchEstimates);
|
||||
}
|
|
@ -16,6 +16,7 @@ import wallet from './wallet';
|
|||
import { ens } from './ens';
|
||||
import { transaction } from './transaction';
|
||||
import transactions from './transactions';
|
||||
import gas from './gas';
|
||||
|
||||
export default {
|
||||
ens,
|
||||
|
@ -35,5 +36,6 @@ export default {
|
|||
deterministicWallets,
|
||||
swapProviderSaga,
|
||||
rates,
|
||||
transactions
|
||||
transactions,
|
||||
gas
|
||||
};
|
||||
|
|
|
@ -5,6 +5,15 @@ $handle-size: 22px;
|
|||
$speed: 70ms;
|
||||
$tooltip-bg: rgba(#222, 0.95);
|
||||
|
||||
@keyframes slider-loading {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-slider {
|
||||
&-rail {
|
||||
background: $gray-lighter;
|
||||
|
@ -39,4 +48,20 @@ $tooltip-bg: rgba(#222, 0.95);
|
|||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled styles
|
||||
&-disabled {
|
||||
background: none;
|
||||
|
||||
.rc-slider {
|
||||
&-handle,
|
||||
&-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-rail {
|
||||
animation: slider-loading 1s ease infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { AppState } from 'reducers';
|
||||
|
||||
const getGas = (state: AppState) => state.gas;
|
||||
export const getEstimates = (state: AppState) => getGas(state).estimates;
|
||||
export const getIsEstimating = (state: AppState) => getGas(state).isEstimating;
|
|
@ -0,0 +1,30 @@
|
|||
import { gas, INITIAL_STATE } from 'reducers/gas';
|
||||
import { fetchGasEstimates, setGasEstimates } from 'actions/gas';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
describe('gas reducer', () => {
|
||||
it('should handle GAS_FETCH_ESTIMATES', () => {
|
||||
const state = gas(undefined, fetchGasEstimates());
|
||||
expect(state).toEqual({
|
||||
...INITIAL_STATE,
|
||||
isEstimating: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle GAS_SET_ESTIMATES', () => {
|
||||
const estimates: GasEstimates = {
|
||||
safeLow: 1,
|
||||
standard: 1,
|
||||
fast: 4,
|
||||
fastest: 20,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
};
|
||||
const state = gas(undefined, setGasEstimates(estimates));
|
||||
expect(state).toEqual({
|
||||
...INITIAL_STATE,
|
||||
estimates,
|
||||
isEstimating: false
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
import { fetchEstimates, setDefaultEstimates } from 'sagas/gas';
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||
import { setGasEstimates } from 'actions/gas';
|
||||
import { getEstimates } from 'selectors/gas';
|
||||
import { getOffline } from 'selectors/config';
|
||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||
|
||||
describe('fetchEstimates*', () => {
|
||||
const gen = cloneableGenerator(fetchEstimates)();
|
||||
const offline = false;
|
||||
const oldEstimates: GasEstimates = {
|
||||
safeLow: 1,
|
||||
standard: 1,
|
||||
fast: 4,
|
||||
fastest: 20,
|
||||
time: Date.now() - gasEstimateCacheTime - 1000,
|
||||
isDefault: false
|
||||
};
|
||||
const newEstimates: GasEstimates = {
|
||||
safeLow: 2,
|
||||
standard: 2,
|
||||
fast: 8,
|
||||
fastest: 80,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
};
|
||||
|
||||
it('Should select getOffline', () => {
|
||||
expect(gen.next().value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('Should use default estimates if offline', () => {
|
||||
const offlineGen = gen.clone();
|
||||
expect(offlineGen.next(true).value).toEqual(call(setDefaultEstimates));
|
||||
expect(offlineGen.next().done).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should select getEstimates', () => {
|
||||
expect(gen.next(offline).value).toEqual(select(getEstimates));
|
||||
});
|
||||
|
||||
it('Should use cached estimates if they’re recent', () => {
|
||||
const cachedGen = gen.clone();
|
||||
const cacheEstimate = {
|
||||
...oldEstimates,
|
||||
time: Date.now() - gasEstimateCacheTime + 1000
|
||||
};
|
||||
expect(cachedGen.next(cacheEstimate).value).toEqual(put(setGasEstimates(cacheEstimate)));
|
||||
expect(cachedGen.next().done).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should fetch new estimates', () => {
|
||||
expect(gen.next(oldEstimates).value).toEqual(call(fetchGasEstimates));
|
||||
});
|
||||
|
||||
it('Should use default estimates if request fails', () => {
|
||||
const failedReqGen = gen.clone();
|
||||
// Not sure why, but typescript seems to think throw might be missing.
|
||||
if (failedReqGen.throw) {
|
||||
expect(failedReqGen.throw('test').value).toEqual(call(setDefaultEstimates));
|
||||
expect(failedReqGen.next().done).toBeTruthy();
|
||||
} else {
|
||||
throw new Error('SagaIterator didn’t have throw');
|
||||
}
|
||||
});
|
||||
|
||||
it('Should use fetched estimates', () => {
|
||||
expect(gen.next(newEstimates).value).toEqual(put(setGasEstimates(newEstimates)));
|
||||
expect(gen.next().done).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDefaultEstimates*', () => {
|
||||
const gen = cloneableGenerator(setDefaultEstimates)();
|
||||
|
||||
it('Should put setGasEstimates with config defaults', () => {
|
||||
const time = Date.now();
|
||||
gen.next();
|
||||
expect(gen.next(time).value).toEqual(
|
||||
put(
|
||||
setGasEstimates({
|
||||
safeLow: gasPriceDefaults.minGwei,
|
||||
standard: gasPriceDefaults.default,
|
||||
fast: gasPriceDefaults.default,
|
||||
fastest: gasPriceDefaults.maxGwei,
|
||||
isDefault: true,
|
||||
time
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue