+
{translate('OFFLINE_Step2_Label_4')}
-
+
+
- {translate('OFFLINE_Step2_Label_5')}
-
+
- {translate('OFFLINE_Step2_Label_6')}
- (
-
- )}
- />
+
@@ -82,14 +80,15 @@ export default class AdvancedGas extends React.Component
{
}
private handleGasPriceChange = (ev: React.FormEvent) => {
- this.props.changeGasPrice(ev.currentTarget.value);
+ this.props.inputGasPrice(ev.currentTarget.value);
};
- private handleGasLimitChange = (ev: React.FormEvent) => {
- this.props.changeGasLimit(ev.currentTarget.value);
- };
-
- private handleNonceChange = (ev: React.FormEvent) => {
- this.props.changeNonce(ev.currentTarget.value);
+ private handleToggleAutoGasLimit = (_: React.FormEvent) => {
+ this.props.toggleAutoGasLimit();
};
}
+
+export default connect(
+ (state: AppState) => ({ autoGasLimitEnabled: getAutoGasLimitEnabled(state) }),
+ { toggleAutoGasLimit }
+)(AdvancedGas);
diff --git a/common/components/GasSlider/components/SimpleGas.scss b/common/components/GasSlider/components/SimpleGas.scss
index cc48f426..cff478f5 100644
--- a/common/components/GasSlider/components/SimpleGas.scss
+++ b/common/components/GasSlider/components/SimpleGas.scss
@@ -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;
+ }
+ }
+}
diff --git a/common/components/GasSlider/components/SimpleGas.tsx b/common/components/GasSlider/components/SimpleGas.tsx
index 4e0eaa06..82bb6f54 100644
--- a/common/components/GasSlider/components/SimpleGas.tsx
+++ b/common/components/GasSlider/components/SimpleGas.tsx
@@ -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 {
+interface StateProps {
+ isWeb3Node: boolean;
+ gasLimitEstimationTimedOut: boolean;
+}
+
+type Props = OwnProps & StateProps;
+
+class SimpleGas extends React.Component {
public render() {
- const { gasPrice } = this.props;
+ const { gasPrice, gasLimitEstimationTimedOut, isWeb3Node } = this.props;
return (
-
+
{translate('Transaction Fee')}
+
+
+ {gasLimitEstimationTimedOut && (
+
+
+ {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"}
+
+
+ )}
+
{translate('Cheap')}
@@ -49,6 +74,10 @@ export default class SimpleGas extends React.Component
{
}
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);
diff --git a/common/components/NonceField.tsx b/common/components/NonceField.tsx
new file mode 100644
index 00000000..2a560182
--- /dev/null
+++ b/common/components/NonceField.tsx
@@ -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 = (
+
+);
+
+export const NonceField: React.SFC = ({ alwaysDisplay }) => (
+ {
+ const content = (
+ <>
+ Nonce
+ {nonceHelp}
+
+
+ >
+ );
+
+ return alwaysDisplay || shouldDisplay ? content : null;
+ }}
+ />
+);
diff --git a/common/components/NonceField/NonceField.tsx b/common/components/NonceField/NonceField.tsx
deleted file mode 100644
index d71a5b9d..00000000
--- a/common/components/NonceField/NonceField.tsx
+++ /dev/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 {
- public render() {
- return ;
- }
-
- private setNonce = (ev: React.FormEvent) => {
- const { value } = ev.currentTarget;
- this.props.inputNonce(value);
- };
-}
-
-export const NonceField = connect(null, {
- inputNonce
-})(NonceFieldClass);
diff --git a/common/components/NonceField/NonceInput.tsx b/common/components/NonceField/NonceInput.tsx
deleted file mode 100644
index 34b404d6..00000000
--- a/common/components/NonceField/NonceInput.tsx
+++ /dev/null
@@ -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 = (
-
-);
-
-interface OwnProps {
- onChange(ev: React.FormEvent): void;
-}
-interface StateProps {
- shouldDisplay: boolean;
- nonce: AppState['transaction']['fields']['nonce'];
-}
-type Props = OwnProps & StateProps;
-
-class NonceInputClass extends Component {
- public render() {
- const { nonce: { raw, value }, onChange, shouldDisplay } = this.props;
- const content = (
-
- Nonce
- {nonceHelp}
-
- (
-
- )}
- />
-
- );
-
- return shouldDisplay ? content : null;
- }
-}
-
-export const NonceInput = connect((state: AppState) => ({
- shouldDisplay: getOffline(state) || nonceRequestFailed(state),
- nonce: getNonce(state)
-}))(NonceInputClass);
diff --git a/common/components/NonceField/index.ts b/common/components/NonceField/index.ts
deleted file mode 100644
index b0b6b9cd..00000000
--- a/common/components/NonceField/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './NonceField';
diff --git a/common/components/NonceFieldFactory/NonceFieldFactory.tsx b/common/components/NonceFieldFactory/NonceFieldFactory.tsx
new file mode 100644
index 00000000..c473b76c
--- /dev/null
+++ b/common/components/NonceFieldFactory/NonceFieldFactory.tsx
@@ -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): void;
+}
+
+interface DispatchProps {
+ inputNonce: TInputNonce;
+}
+
+interface OwnProps {
+ withProps(props: CallbackProps): React.ReactElement | null;
+}
+
+type Props = OwnProps & DispatchProps;
+
+class NonceFieldClass extends Component {
+ public render() {
+ return ;
+ }
+
+ private setNonce = (ev: React.FormEvent) => {
+ const { value } = ev.currentTarget;
+ this.props.inputNonce(value);
+ };
+}
+
+export const NonceFieldFactory = connect(null, {
+ inputNonce
+})(NonceFieldClass);
diff --git a/common/components/NonceFieldFactory/NonceInputFactory.tsx b/common/components/NonceFieldFactory/NonceInputFactory.tsx
new file mode 100644
index 00000000..57349135
--- /dev/null
+++ b/common/components/NonceFieldFactory/NonceInputFactory.tsx
@@ -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): void;
+ withProps(props: CallbackProps): React.ReactElement | null;
+}
+
+interface StateProps {
+ shouldDisplay: boolean;
+ nonce: AppState['transaction']['fields']['nonce'];
+}
+
+type Props = OwnProps & StateProps;
+
+class NonceInputFactoryClass extends Component {
+ public render() {
+ const { nonce, onChange, shouldDisplay, withProps } = this.props;
+
+ return (
+
+ withProps({ nonce, onChange, readOnly: !!readOnly, shouldDisplay })
+ }
+ />
+ );
+ }
+}
+
+export const NonceInputFactory = connect((state: AppState) => ({
+ shouldDisplay: getOffline(state) || nonceRequestFailed(state),
+ nonce: getNonce(state)
+}))(NonceInputFactoryClass);
diff --git a/common/components/NonceFieldFactory/index.ts b/common/components/NonceFieldFactory/index.ts
new file mode 100644
index 00000000..43f0432e
--- /dev/null
+++ b/common/components/NonceFieldFactory/index.ts
@@ -0,0 +1 @@
+export * from './NonceFieldFactory';
diff --git a/common/containers/Tabs/Contracts/components/Deploy.tsx b/common/containers/Tabs/Contracts/components/Deploy.tsx
index 2672b2a8..fff1eec6 100644
--- a/common/containers/Tabs/Contracts/components/Deploy.tsx
+++ b/common/containers/Tabs/Contracts/components/Deploy.tsx
@@ -64,7 +64,7 @@ class DeployClass extends Component {
diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/components/Fields.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/components/Fields.tsx
index 21b6722c..f30dbe31 100644
--- a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/components/Fields.tsx
+++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/components/Fields.tsx
@@ -14,7 +14,7 @@ export class Fields extends Component
{
-
+
{this.props.button}
diff --git a/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx
index bb04e1b3..dfd6b7a7 100644
--- a/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx
+++ b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx
@@ -106,7 +106,7 @@ class RequestPayment extends React.Component {
diff --git a/common/libs/nodes/rpc/index.ts b/common/libs/nodes/rpc/index.ts
index 038a64a6..ac4eb764 100644
--- a/common/libs/nodes/rpc/index.ts
+++ b/common/libs/nodes/rpc/index.ts
@@ -46,10 +46,15 @@ export default class RpcNode implements INode {
}
public estimateGas(transaction: Partial): Promise {
+ // 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(
diff --git a/common/reducers/config.ts b/common/reducers/config.ts
index e218c2eb..a03a8dda 100644
--- a/common/reducers/config.ts
+++ b/common/reducers/config.ts
@@ -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:
diff --git a/common/reducers/transaction/network/network.ts b/common/reducers/transaction/network/network.ts
index 8658ddd0..565da9a1 100644
--- a/common/reducers/transaction/network/network.ts
+++ b/common/reducers/transaction/network/network.ts
@@ -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:
diff --git a/common/reducers/transaction/network/typings.ts b/common/reducers/transaction/network/typings.ts
index 57add313..6a830461 100644
--- a/common/reducers/transaction/network/typings.ts
+++ b/common/reducers/transaction/network/typings.ts
@@ -1,7 +1,8 @@
export enum RequestStatus {
REQUESTED = 'PENDING',
SUCCEEDED = 'SUCCESS',
- FAILED = 'FAIL'
+ FAILED = 'FAIL',
+ TIMEDOUT = 'TIMEDOUT'
}
export interface State {
gasEstimationStatus: RequestStatus | null;
diff --git a/common/sagas/transaction/network/gas.ts b/common/sagas/transaction/network/gas.ts
index 997b71d9..fdfd5565 100644
--- a/common/sagas/transaction/network/gas.ts
+++ b/common/sagas/transaction/network/gas.ts
@@ -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)];
diff --git a/common/selectors/config.ts b/common/selectors/config.ts
index a684d721..9039d386 100644
--- a/common/selectors/config.ts
+++ b/common/selectors/config.ts
@@ -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);
diff --git a/common/selectors/transaction/network.ts b/common/selectors/transaction/network.ts
index 5a6be4d6..d6ec4890 100644
--- a/common/selectors/transaction/network.ts
+++ b/common/selectors/transaction/network.ts
@@ -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;
diff --git a/common/store.ts b/common/store.ts
index 6bc9a7e9..4827592f 100644
--- a/common/store.ts
+++ b/common/store.ts
@@ -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: {
diff --git a/spec/sagas/transaction/network/gas.spec.ts b/spec/sagas/transaction/network/gas.spec.ts
index 0f6aaadf..4469b659 100644
--- a/spec/sagas/transaction/network/gas.spec.ts
+++ b/spec/sagas/transaction/network/gas.spec.ts
@@ -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
+ })
+ )
+ );
+ });
+});