{gasError && this.renderError(gasError)}
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
- {error && this.renderError()}
+ {isAssetSendable === false &&
+ this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
+ {error && this.renderError(error)}
{warning && this.renderWarning()}
{this.maybeRenderAddContact()}
-
+
- {this.props.showHexData && (
-
- )}
+ {this.props.showHexData && }
);
@@ -97,12 +97,11 @@ export default class SendContent extends Component {
);
}
- renderError(gasError = '') {
+ renderError(error) {
const { t } = this.context;
- const { error } = this.props;
return (
{
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Clicked "Advanced Options"',
- },
+ trackEvent({
+ category: 'Transactions',
+ event: 'Clicked "Advanced Options"',
});
showCustomizeGasModal();
}}
@@ -69,44 +48,22 @@ export default class SendGasRow extends Component {
);
}
- setMaxAmount() {
- const {
- balance,
- gasTotal,
- sendToken,
- setAmountToMax,
- tokenBalance,
- } = this.props;
-
- setAmountToMax({
- balance,
- gasTotal,
- sendToken,
- tokenBalance,
- });
- }
-
renderContent() {
const {
gasLoadingError,
gasTotal,
showCustomizeGasModal,
gasPriceButtonGroupProps,
- gasButtonGroupShown,
- advancedInlineGasShown,
- maxModeOn,
+ gasInputMode,
resetGasButtons,
- setGasPrice,
- setGasLimit,
+ updateGasPrice,
+ updateGasLimit,
gasPrice,
gasLimit,
insufficientBalance,
- isMainnet,
- isEthGasPrice,
- noGasPrice,
+ minimumGasLimit,
} = this.props;
- const { metricsEvent } = this.context;
- const gasPriceFetchFailure = isEthGasPrice || noGasPrice;
+ const { trackEvent } = this.context;
const gasPriceButtonGroup = (
@@ -115,17 +72,14 @@ export default class SendGasRow extends Component {
showCheck={false}
{...gasPriceButtonGroupProps}
handleGasPriceSelection={async (opts) => {
- metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Changed Gas Button',
+ trackEvent({
+ category: 'Transactions',
+ event: 'User Clicked Gas Estimate Button',
+ properties: {
+ gasEstimateType: opts.gasEstimateType.toLowerCase(),
},
});
await gasPriceButtonGroupProps.handleGasPriceSelection(opts);
- if (maxModeOn) {
- this.setMaxAmount();
- }
}}
/>
@@ -134,51 +88,38 @@ export default class SendGasRow extends Component {
{
- resetGasButtons();
- if (maxModeOn) {
- this.setMaxAmount();
- }
- }}
- onClick={() => showCustomizeGasModal()}
+ onReset={resetGasButtons}
+ onClick={showCustomizeGasModal}
/>
);
const advancedGasInputs = (
- setGasPrice({ gasPrice: newGasPrice, gasLimit })
- }
- updateCustomGasLimit={(newGasLimit) =>
- setGasLimit(newGasLimit, gasPrice)
- }
+ updateCustomGasPrice={updateGasPrice}
+ updateCustomGasLimit={updateGasLimit}
customGasPrice={gasPrice}
customGasLimit={gasLimit}
insufficientBalance={insufficientBalance}
+ minimumGasLimit={minimumGasLimit}
customPriceIsSafe
isSpeedUp={false}
/>
);
// Tests should behave in same way as mainnet, but are using Localhost
- if (
- advancedInlineGasShown ||
- (!isMainnet && !process.env.IN_TEST) ||
- gasPriceFetchFailure
- ) {
- return advancedGasInputs;
- } else if (gasButtonGroupShown) {
- return gasPriceButtonGroup;
+ switch (gasInputMode) {
+ case GAS_INPUT_MODES.BASIC:
+ return gasPriceButtonGroup;
+ case GAS_INPUT_MODES.INLINE:
+ return advancedGasInputs;
+ case GAS_INPUT_MODES.CUSTOM:
+ default:
+ return gasFeeDisplay;
}
- return gasFeeDisplay;
}
render() {
- const {
- gasFeeError,
- gasButtonGroupShown,
- advancedInlineGasShown,
- } = this.props;
+ const { gasFeeError, gasInputMode, advancedInlineGasShown } = this.props;
return (
<>
@@ -189,7 +130,7 @@ export default class SendGasRow extends Component {
>
{this.renderContent()}
- {gasButtonGroupShown || advancedInlineGasShown ? (
+ {gasInputMode === GAS_INPUT_MODES.BASIC || advancedInlineGasShown ? (
{this.renderAdvancedOptionsButton()}
) : null}
>
diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.component.test.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.component.test.js
index b6382f5bf..9c5cfa30f 100644
--- a/ui/pages/send/send-content/send-gas-row/send-gas-row.component.test.js
+++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.component.test.js
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import sinon from 'sinon';
import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component';
import GasPriceButtonGroup from '../../../../components/app/gas-customization/gas-price-button-group';
+import { GAS_INPUT_MODES } from '../../../../ducks/send';
import SendGasRow from './send-gas-row.component';
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component';
@@ -24,7 +25,7 @@ describe('SendGasRow Component', () => {
gasFeeError
gasLoadingError={false}
gasTotal="mockGasTotal"
- gasButtonGroupShown={false}
+ gasInputMode={GAS_INPUT_MODES.CUSTOM}
showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
resetGasButtons={propsMethodSpies.resetGasButtons}
gasPriceButtonGroupProps={{
@@ -32,7 +33,7 @@ describe('SendGasRow Component', () => {
anotherGasPriceButtonGroupProp: 'bar',
}}
/>,
- { context: { t: (str) => `${str}_t`, metricsEvent: () => ({}) } },
+ { context: { t: (str) => `${str}_t`, trackEvent: () => ({}) } },
);
wrapper.setProps({ isMainnet: true });
});
@@ -76,8 +77,8 @@ describe('SendGasRow Component', () => {
expect(propsMethodSpies.resetGasButtons.callCount).toStrictEqual(1);
});
- it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => {
- wrapper.setProps({ gasButtonGroupShown: true });
+ it('should render the GasPriceButtonGroup if gasInputMode is BASIC', () => {
+ wrapper.setProps({ gasInputMode: GAS_INPUT_MODES.BASIC });
const rendered = wrapper.find(SendRowWrapper).first().childAt(0);
expect(wrapper.children()).toHaveLength(2);
@@ -95,8 +96,8 @@ describe('SendGasRow Component', () => {
).toStrictEqual('bar');
});
- it('should render an advanced options button if gasButtonGroupShown is true', () => {
- wrapper.setProps({ gasButtonGroupShown: true });
+ it('should render an advanced options button if gasInputMode is BASIC', () => {
+ wrapper.setProps({ gasInputMode: GAS_INPUT_MODES.BASIC });
const rendered = wrapper.find(SendRowWrapper).last();
expect(wrapper.children()).toHaveLength(2);
diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js
index 84d6886fb..189fa95e4 100644
--- a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js
+++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js
@@ -1,42 +1,30 @@
import { connect } from 'react-redux';
+import {
+ getBasicGasEstimateLoadingStatus,
+ getRenderableEstimateDataForSmallButtonsFromGWEI,
+ getDefaultActiveButtonIndex,
+ getAdvancedInlineGasShown,
+} from '../../../../selectors';
import {
getGasTotal,
getGasPrice,
getGasLimit,
- getSendAmount,
- getSendFromBalance,
- getTokenBalance,
- getSendMaxModeState,
- getGasLoadingError,
gasFeeIsInError,
- getGasButtonGroupShown,
- getAdvancedInlineGasShown,
- getCurrentEthBalance,
- getSendToken,
- getBasicGasEstimateLoadingStatus,
- getRenderableEstimateDataForSmallButtonsFromGWEI,
- getDefaultActiveButtonIndex,
- getIsMainnet,
- getIsEthGasPriceFetched,
- getNoGasPriceFetched,
-} from '../../../../selectors';
-import { isBalanceSufficient, calcGasTotal } from '../../send.utils';
-import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-button.utils';
-import {
- showGasButtonGroup,
- updateSendErrors,
- setGasPrice,
- setGasLimit,
- setGasTotal,
- updateSendAmount,
-} from '../../../../ducks/send/send.duck';
+ getGasInputMode,
+ updateGasPrice,
+ updateGasLimit,
+ isSendStateInitialized,
+ getIsBalanceInsufficient,
+ getMinimumGasLimitForSend,
+ useDefaultGas,
+} from '../../../../ducks/send';
import {
resetCustomData,
setCustomGasPrice,
setCustomGasLimit,
} from '../../../../ducks/gas/gas.duck';
-import { getConversionRate } from '../../../../ducks/metamask/metamask';
import { showModal } from '../../../../store/actions';
+import { hexToDecimal } from '../../../../helpers/utils/conversions.util';
import SendGasRow from './send-gas-row.component';
export default connect(
@@ -55,40 +43,25 @@ function mapStateToProps(state) {
);
const gasTotal = getGasTotal(state);
- const conversionRate = getConversionRate(state);
- const balance = getCurrentEthBalance(state);
- const insufficientBalance = !isBalanceSufficient({
- amount: getSendToken(state) ? '0x0' : getSendAmount(state),
- gasTotal,
- balance,
- conversionRate,
- });
- const isEthGasPrice = getIsEthGasPriceFetched(state);
- const noGasPrice = getNoGasPriceFetched(state);
+ const minimumGasLimit = getMinimumGasLimitForSend(state);
return {
- balance: getSendFromBalance(state),
gasTotal,
+ minimumGasLimit: hexToDecimal(minimumGasLimit),
gasFeeError: gasFeeIsInError(state),
- gasLoadingError: getGasLoadingError(state),
+ gasLoadingError: isSendStateInitialized(state),
gasPriceButtonGroupProps: {
buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
defaultActiveButtonIndex: 1,
newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null,
gasButtonInfo,
},
- gasButtonGroupShown: getGasButtonGroupShown(state),
advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ gasInputMode: getGasInputMode(state),
gasPrice,
gasLimit,
- insufficientBalance,
- maxModeOn: getSendMaxModeState(state),
- sendToken: getSendToken(state),
- tokenBalance: getTokenBalance(state),
- isMainnet: getIsMainnet(state),
- isEthGasPrice,
- noGasPrice,
+ insufficientBalance: getIsBalanceInsufficient(state),
};
}
@@ -96,26 +69,16 @@ function mapDispatchToProps(dispatch) {
return {
showCustomizeGasModal: () =>
dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
- setGasPrice: ({ gasPrice, gasLimit }) => {
- dispatch(setGasPrice(gasPrice));
+ updateGasPrice: (gasPrice) => {
+ dispatch(updateGasPrice(gasPrice));
dispatch(setCustomGasPrice(gasPrice));
- if (gasLimit) {
- dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)));
- }
},
- setGasLimit: (newLimit, gasPrice) => {
- dispatch(setGasLimit(newLimit));
+ updateGasLimit: (newLimit) => {
+ dispatch(updateGasLimit(newLimit));
dispatch(setCustomGasLimit(newLimit));
- if (gasPrice) {
- dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)));
- }
},
- setAmountToMax: (maxAmountDataObject) => {
- dispatch(updateSendErrors({ amount: null }));
- dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)));
- },
- showGasButtonGroup: () => dispatch(showGasButtonGroup()),
resetCustomData: () => dispatch(resetCustomData()),
+ useDefaultGas: () => dispatch(useDefaultGas()),
};
}
@@ -123,8 +86,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
const { gasPriceButtonGroupProps } = stateProps;
const { gasButtonInfo } = gasPriceButtonGroupProps;
const {
- setGasPrice: dispatchSetGasPrice,
- showGasButtonGroup: dispatchShowGasButtonGroup,
+ updateGasPrice: dispatchUpdateGasPrice,
+ useDefaultGas: dispatchUseDefaultGas,
resetCustomData: dispatchResetCustomData,
...otherDispatchProps
} = dispatchProps;
@@ -135,13 +98,14 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
...ownProps,
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
- handleGasPriceSelection: dispatchSetGasPrice,
+ handleGasPriceSelection: ({ gasPrice }) =>
+ dispatchUpdateGasPrice(gasPrice),
},
resetGasButtons: () => {
dispatchResetCustomData();
- dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei);
- dispatchShowGasButtonGroup();
+ dispatchUpdateGasPrice(gasButtonInfo[1].priceInHexWei);
+ dispatchUseDefaultGas();
},
- setGasPrice: dispatchSetGasPrice,
+ updateGasPrice: dispatchUpdateGasPrice,
};
}
diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js
index 80757f230..b7aa23c86 100644
--- a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js
+++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js
@@ -8,12 +8,7 @@ import {
setCustomGasLimit,
} from '../../../../ducks/gas/gas.duck';
-import {
- showGasButtonGroup,
- setGasPrice,
- setGasTotal,
- setGasLimit,
-} from '../../../../ducks/send/send.duck';
+import { updateGasPrice, updateGasLimit } from '../../../../ducks/send';
let mapDispatchToProps;
let mergeProps;
@@ -26,9 +21,15 @@ jest.mock('react-redux', () => ({
},
}));
-jest.mock('../../../../selectors', () => ({
- getSendMaxModeState: (s) => `mockMaxModeOn:${s}`,
-}));
+jest.mock('../../../../ducks/send', () => {
+ const original = jest.requireActual('../../../../ducks/send');
+ return {
+ ...original,
+ getSendMaxModeState: (s) => `mockMaxModeOn:${s}`,
+ updateGasPrice: jest.fn(),
+ updateGasLimit: jest.fn(),
+ };
+});
jest.mock('../../send.utils.js', () => ({
isBalanceSufficient: ({ amount, gasTotal, balance, conversionRate }) =>
@@ -41,13 +42,6 @@ jest.mock('../../../../store/actions', () => ({
showModal: jest.fn(),
}));
-jest.mock('../../../../ducks/send/send.duck', () => ({
- showGasButtonGroup: jest.fn(),
- setGasPrice: jest.fn(),
- setGasTotal: jest.fn(),
- setGasLimit: jest.fn(),
-}));
-
jest.mock('../../../../ducks/gas/gas.duck', () => ({
resetCustomData: jest.fn(),
setCustomGasPrice: jest.fn(),
@@ -77,36 +71,21 @@ describe('send-gas-row container', () => {
});
});
- describe('setGasPrice()', () => {
+ describe('updateGasPrice()', () => {
it('should dispatch an action', () => {
- mapDispatchToPropsObject.setGasPrice({
- gasPrice: 'mockNewPrice',
- gasLimit: 'mockLimit',
- });
- expect(dispatchSpy.calledThrice).toStrictEqual(true);
- expect(setGasPrice).toHaveBeenCalled();
+ mapDispatchToPropsObject.updateGasPrice('mockNewPrice');
+ expect(dispatchSpy.calledTwice).toStrictEqual(true);
+ expect(updateGasPrice).toHaveBeenCalled();
expect(setCustomGasPrice).toHaveBeenCalledWith('mockNewPrice');
- expect(setGasTotal).toHaveBeenCalled();
- expect(setGasTotal).toHaveBeenCalledWith('mockLimitmockNewPrice');
});
});
- describe('setGasLimit()', () => {
+ describe('updateGasLimit()', () => {
it('should dispatch an action', () => {
- mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice');
- expect(dispatchSpy.calledThrice).toStrictEqual(true);
- expect(setGasLimit).toHaveBeenCalled();
+ mapDispatchToPropsObject.updateGasLimit('mockNewLimit');
+ expect(dispatchSpy.calledTwice).toStrictEqual(true);
+ expect(updateGasLimit).toHaveBeenCalled();
expect(setCustomGasLimit).toHaveBeenCalledWith('mockNewLimit');
- expect(setGasTotal).toHaveBeenCalled();
- expect(setGasTotal).toHaveBeenCalledWith('mockNewLimitmockPrice');
- });
- });
-
- describe('showGasButtonGroup()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.showGasButtonGroup();
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(showGasButtonGroup).toHaveBeenCalled();
});
});
@@ -129,7 +108,7 @@ describe('send-gas-row container', () => {
someOtherStateProp: 'baz',
};
const dispatchProps = {
- setGasPrice: sinon.spy(),
+ updateGasPrice: sinon.spy(),
someOtherDispatchProp: sinon.spy(),
};
const ownProps = { someOwnProp: 123 };
@@ -144,9 +123,11 @@ describe('send-gas-row container', () => {
).toStrictEqual('bar');
expect(result.someOwnProp).toStrictEqual(123);
- expect(dispatchProps.setGasPrice.callCount).toStrictEqual(0);
- result.gasPriceButtonGroupProps.handleGasPriceSelection();
- expect(dispatchProps.setGasPrice.callCount).toStrictEqual(1);
+ expect(dispatchProps.updateGasPrice.callCount).toStrictEqual(0);
+ result.gasPriceButtonGroupProps.handleGasPriceSelection({
+ gasPrice: undefined,
+ });
+ expect(dispatchProps.updateGasPrice.callCount).toStrictEqual(1);
expect(dispatchProps.someOtherDispatchProp.callCount).toStrictEqual(0);
result.someOtherDispatchProp();
diff --git a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js
index 080b97b37..bcb6530f8 100644
--- a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js
+++ b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js
@@ -6,7 +6,6 @@ export default class SendHexDataRow extends Component {
static propTypes = {
inError: PropTypes.bool,
updateSendHexData: PropTypes.func.isRequired,
- updateGas: PropTypes.func.isRequired,
};
static contextTypes = {
@@ -14,10 +13,9 @@ export default class SendHexDataRow extends Component {
};
onInput = (event) => {
- const { updateSendHexData, updateGas } = this.props;
+ const { updateSendHexData } = this.props;
const data = event.target.value.replace(/\n/gu, '') || null;
updateSendHexData(data);
- updateGas({ data });
};
render() {
@@ -32,7 +30,7 @@ export default class SendHexDataRow extends Component {
>
diff --git a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js
index f645aff7a..044f3eb69 100644
--- a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js
+++ b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js
@@ -1,12 +1,12 @@
import { connect } from 'react-redux';
-import { updateSendHexData } from '../../../../ducks/send/send.duck';
+import { getSendHexData, updateSendHexData } from '../../../../ducks/send';
import SendHexDataRow from './send-hex-data-row.component';
export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow);
function mapStateToProps(state) {
return {
- data: state.send.data,
+ data: getSendHexData(state),
};
}
diff --git a/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
index f857183f3..45a208537 100644
--- a/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
+++ b/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import { getSendErrors } from '../../../../../selectors';
+import { getSendErrors } from '../../../../../ducks/send';
import SendRowErrorMessage from './send-row-error-message.component';
export default connect(mapStateToProps)(SendRowErrorMessage);
diff --git a/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.test.js b/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.test.js
index a8012f200..23f1d2c68 100644
--- a/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.test.js
+++ b/ui/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.test.js
@@ -8,7 +8,7 @@ jest.mock('react-redux', () => ({
},
}));
-jest.mock('../../../../../selectors', () => ({
+jest.mock('../../../../../ducks/send', () => ({
getSendErrors: (s) => `mockErrors:${s}`,
}));
diff --git a/ui/pages/send/send-footer/send-footer.component.js b/ui/pages/send/send-footer/send-footer.component.js
index ef18f48b1..840146f5f 100644
--- a/ui/pages/send/send-footer/send-footer.component.js
+++ b/ui/pages/send/send-footer/send-footer.component.js
@@ -1,33 +1,21 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
+import { isEqual } from 'lodash';
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes';
export default class SendFooter extends Component {
static propTypes = {
addToAddressBookIfNew: PropTypes.func,
- amount: PropTypes.string,
- data: PropTypes.string,
- clearSend: PropTypes.func,
- editingTransactionId: PropTypes.string,
- from: PropTypes.object,
- gasLimit: PropTypes.string,
- gasPrice: PropTypes.string,
- gasTotal: PropTypes.string,
+ resetSendState: PropTypes.func,
+ disabled: PropTypes.bool.isRequired,
history: PropTypes.object,
- inError: PropTypes.bool,
- sendToken: PropTypes.object,
sign: PropTypes.func,
to: PropTypes.string,
toAccounts: PropTypes.array,
- tokenBalance: PropTypes.string,
- unapprovedTxs: PropTypes.object,
- update: PropTypes.func,
sendErrors: PropTypes.object,
gasEstimateType: PropTypes.string,
- gasIsLoading: PropTypes.bool,
mostRecentOverviewPage: PropTypes.string.isRequired,
- noGasPrice: PropTypes.bool,
};
static contextTypes = {
@@ -36,8 +24,8 @@ export default class SendFooter extends Component {
};
onCancel() {
- const { clearSend, history, mostRecentOverviewPage } = this.props;
- clearSend();
+ const { resetSendState, history, mostRecentOverviewPage } = this.props;
+ resetSendState();
history.push(mostRecentOverviewPage);
}
@@ -45,45 +33,17 @@ export default class SendFooter extends Component {
event.preventDefault();
const {
addToAddressBookIfNew,
- amount,
- data,
- editingTransactionId,
- from: { address: from },
- gasLimit: gas,
- gasPrice,
- sendToken,
sign,
to,
- unapprovedTxs,
- update,
toAccounts,
history,
gasEstimateType,
} = this.props;
const { metricsEvent } = this.context;
- // Should not be needed because submit should be disabled if there are errors.
- // const noErrors = !amountError && toError === null
-
- // if (!noErrors) {
- // return
- // }
-
// TODO: add nickname functionality
await addToAddressBookIfNew(to, toAccounts);
- const promise = editingTransactionId
- ? update({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- sendToken,
- to,
- unapprovedTxs,
- })
- : sign({ data, sendToken, to, amount, from, gas, gasPrice });
+ const promise = sign();
Promise.resolve(promise).then(() => {
metricsEvent({
@@ -100,35 +60,13 @@ export default class SendFooter extends Component {
});
}
- formShouldBeDisabled() {
- const {
- data,
- inError,
- sendToken,
- tokenBalance,
- gasTotal,
- to,
- gasLimit,
- gasIsLoading,
- noGasPrice,
- } = this.props;
- const missingTokenBalance = sendToken && !tokenBalance;
- const gasLimitTooLow = gasLimit < 5208; // 5208 is hex value of 21000, minimum gas limit
- const shouldBeDisabled =
- inError ||
- !gasTotal ||
- missingTokenBalance ||
- !(data || to) ||
- gasLimitTooLow ||
- gasIsLoading ||
- noGasPrice;
- return shouldBeDisabled;
- }
-
componentDidUpdate(prevProps) {
- const { inError, sendErrors } = this.props;
+ const { sendErrors } = this.props;
const { metricsEvent } = this.context;
- if (!prevProps.inError && inError) {
+ if (
+ Object.keys(sendErrors).length > 0 &&
+ isEqual(sendErrors, prevProps.sendErrors) === false
+ ) {
const errorField = Object.keys(sendErrors).find((key) => sendErrors[key]);
const errorMessage = sendErrors[errorField];
@@ -151,7 +89,7 @@ export default class SendFooter extends Component {
this.onCancel()}
onSubmit={(e) => this.onSubmit(e)}
- disabled={this.formShouldBeDisabled()}
+ disabled={this.props.disabled}
/>
);
}
diff --git a/ui/pages/send/send-footer/send-footer.component.test.js b/ui/pages/send/send-footer/send-footer.component.test.js
index 900c26b2a..fcd4472d6 100644
--- a/ui/pages/send/send-footer/send-footer.component.test.js
+++ b/ui/pages/send/send-footer/send-footer.component.test.js
@@ -10,7 +10,7 @@ describe('SendFooter Component', () => {
const propsMethodSpies = {
addToAddressBookIfNew: sinon.spy(),
- clearSend: sinon.spy(),
+ resetSendState: sinon.spy(),
sign: sinon.spy(),
update: sinon.spy(),
mostRecentOverviewPage: '/',
@@ -29,36 +29,24 @@ describe('SendFooter Component', () => {
wrapper = shallow(
,
{ context: { t: (str) => str, metricsEvent: () => ({}) } },
);
});
afterEach(() => {
- propsMethodSpies.clearSend.resetHistory();
+ propsMethodSpies.resetSendState.resetHistory();
propsMethodSpies.addToAddressBookIfNew.resetHistory();
- propsMethodSpies.clearSend.resetHistory();
+ propsMethodSpies.resetSendState.resetHistory();
propsMethodSpies.sign.resetHistory();
propsMethodSpies.update.resetHistory();
historySpies.push.resetHistory();
@@ -71,10 +59,10 @@ describe('SendFooter Component', () => {
});
describe('onCancel', () => {
- it('should call clearSend', () => {
- expect(propsMethodSpies.clearSend.callCount).toStrictEqual(0);
+ it('should call resetSendState', () => {
+ expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(0);
wrapper.instance().onCancel();
- expect(propsMethodSpies.clearSend.callCount).toStrictEqual(1);
+ expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(1);
});
it('should call history.push', () => {
@@ -87,59 +75,6 @@ describe('SendFooter Component', () => {
});
});
- describe('formShouldBeDisabled()', () => {
- const config = {
- 'should return true if inError is truthy': {
- inError: true,
- expectedResult: true,
- gasIsLoading: false,
- },
- 'should return true if gasTotal is falsy': {
- inError: false,
- gasTotal: '',
- expectedResult: true,
- gasIsLoading: false,
- },
- 'should return true if to is truthy': {
- to: '0xsomevalidAddress',
- inError: false,
- gasTotal: '',
- expectedResult: true,
- gasIsLoading: false,
- },
- 'should return true if sendToken is truthy and tokenBalance is falsy': {
- sendToken: { mockProp: 'mockSendTokenProp' },
- tokenBalance: '',
- expectedResult: true,
- gasIsLoading: false,
- },
- 'should return true if gasIsLoading is truthy but all other params are falsy': {
- inError: false,
- gasTotal: '',
- sendToken: null,
- tokenBalance: '',
- expectedResult: true,
- gasIsLoading: true,
- },
- 'should return false if inError is false and all other params are truthy': {
- inError: false,
- gasTotal: '0x123',
- sendToken: { mockProp: 'mockSendTokenProp' },
- tokenBalance: '123',
- expectedResult: false,
- gasIsLoading: false,
- },
- };
- Object.entries(config).forEach(([description, obj]) => {
- it(`${description}`, () => {
- wrapper.setProps(obj);
- expect(wrapper.instance().formShouldBeDisabled()).toStrictEqual(
- obj.expectedResult,
- );
- });
- });
- });
-
describe('onSubmit', () => {
it('should call addToAddressBookIfNew with the correct params', () => {
wrapper.instance().onSubmit(MOCK_EVENT);
@@ -151,43 +86,9 @@ describe('SendFooter Component', () => {
).toStrictEqual(['mockTo', ['mockAccount']]);
});
- it('should call props.update if editingTransactionId is truthy', async () => {
- await wrapper.instance().onSubmit(MOCK_EVENT);
- expect(propsMethodSpies.update.calledOnce).toStrictEqual(true);
- expect(propsMethodSpies.update.getCall(0).args[0]).toStrictEqual({
- data: undefined,
- amount: 'mockAmount',
- editingTransactionId: 'mockEditingTransactionId',
- from: 'mockAddress',
- gas: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- sendToken: { mockProp: 'mockSendTokenProp' },
- to: 'mockTo',
- unapprovedTxs: {},
- });
- });
-
- it('should not call props.sign if editingTransactionId is truthy', () => {
- expect(propsMethodSpies.sign.callCount).toStrictEqual(0);
- });
-
- it('should call props.sign if editingTransactionId is falsy', async () => {
- wrapper.setProps({ editingTransactionId: null });
+ it('should call props.sign whe submitting', async () => {
await wrapper.instance().onSubmit(MOCK_EVENT);
expect(propsMethodSpies.sign.calledOnce).toStrictEqual(true);
- expect(propsMethodSpies.sign.getCall(0).args[0]).toStrictEqual({
- data: undefined,
- amount: 'mockAmount',
- from: 'mockAddress',
- gas: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- sendToken: { mockProp: 'mockSendTokenProp' },
- to: 'mockTo',
- });
- });
-
- it('should not call props.update if editingTransactionId is falsy', () => {
- expect(propsMethodSpies.update.callCount).toStrictEqual(0);
});
it('should call history.push', async () => {
@@ -201,12 +102,11 @@ describe('SendFooter Component', () => {
describe('render', () => {
beforeEach(() => {
- sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns(true);
wrapper = shallow(
{
gasPrice="mockGasPrice"
gasTotal="mockGasTotal"
history={historySpies}
- inError={false}
sendToken={{ mockProp: 'mockSendTokenProp' }}
sign={propsMethodSpies.sign}
to="mockTo"
@@ -229,10 +128,6 @@ describe('SendFooter Component', () => {
);
});
- afterEach(() => {
- SendFooter.prototype.formShouldBeDisabled.restore();
- });
-
it('should render a PageContainerFooter component', () => {
expect(wrapper.find(PageContainerFooter)).toHaveLength(1);
});
diff --git a/ui/pages/send/send-footer/send-footer.container.js b/ui/pages/send/send-footer/send-footer.container.js
index 8848255d3..bcdb796e1 100644
--- a/ui/pages/send/send-footer/send-footer.container.js
+++ b/ui/pages/send/send-footer/send-footer.container.js
@@ -1,44 +1,32 @@
import { connect } from 'react-redux';
+import { addToAddressBook } from '../../../store/actions';
import {
- addToAddressBook,
- signTokenTx,
- signTx,
- updateTransaction,
-} from '../../../store/actions';
-import {
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getSendToken,
- getSendAmount,
- getSendEditingTransactionId,
- getSendFromObject,
- getSendTo,
- getSendHexData,
- getTokenBalance,
- getSendErrors,
- isSendFormInError,
- getGasIsLoading,
getRenderableEstimateDataForSmallButtonsFromGWEI,
getDefaultActiveButtonIndex,
- getNoGasPriceFetched,
} from '../../../selectors';
+import {
+ resetSendState,
+ getGasPrice,
+ getSendTo,
+ getSendErrors,
+ isSendFormInvalid,
+ signTransaction,
+} from '../../../ducks/send';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { addHexPrefix } from '../../../../app/scripts/lib/util';
-import {
- getSendToAccounts,
- getUnapprovedTxs,
-} from '../../../ducks/metamask/metamask';
-import { clearSend } from '../../../ducks/send/send.duck';
+import { getSendToAccounts } from '../../../ducks/metamask/metamask';
import SendFooter from './send-footer.component';
-import {
- addressIsNew,
- constructTxParams,
- constructUpdatedTx,
-} from './send-footer.utils';
export default connect(mapStateToProps, mapDispatchToProps)(SendFooter);
+function addressIsNew(toAccounts, newAddress) {
+ const newAddressNormalized = newAddress.toLowerCase();
+ const foundMatching = toAccounts.some(
+ ({ address }) => address.toLowerCase() === newAddressNormalized,
+ );
+ return !foundMatching;
+}
+
function mapStateToProps(state) {
const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state);
const gasPrice = getGasPrice(state);
@@ -50,74 +38,21 @@ function mapStateToProps(state) {
activeButtonIndex >= 0
? gasButtonInfo[activeButtonIndex].gasEstimateType
: 'custom';
- const editingTransactionId = getSendEditingTransactionId(state);
return {
- amount: getSendAmount(state),
- data: getSendHexData(state),
- editingTransactionId,
- from: getSendFromObject(state),
- gasLimit: getGasLimit(state),
- gasPrice: getGasPrice(state),
- gasTotal: getGasTotal(state),
- inError: isSendFormInError(state),
- sendToken: getSendToken(state),
+ disabled: isSendFormInvalid(state),
to: getSendTo(state),
toAccounts: getSendToAccounts(state),
- tokenBalance: getTokenBalance(state),
- unapprovedTxs: getUnapprovedTxs(state),
sendErrors: getSendErrors(state),
gasEstimateType,
- gasIsLoading: getGasIsLoading(state),
mostRecentOverviewPage: getMostRecentOverviewPage(state),
- noGasPrice: getNoGasPriceFetched(state),
};
}
function mapDispatchToProps(dispatch) {
return {
- clearSend: () => dispatch(clearSend()),
- sign: ({ sendToken, to, amount, from, gas, gasPrice, data }) => {
- const txParams = constructTxParams({
- amount,
- data,
- from,
- gas,
- gasPrice,
- sendToken,
- to,
- });
-
- return sendToken
- ? dispatch(signTokenTx(sendToken.address, to, amount, txParams))
- : dispatch(signTx(txParams));
- },
- update: ({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- sendToken,
- to,
- unapprovedTxs,
- }) => {
- const editingTx = constructUpdatedTx({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- sendToken,
- to,
- unapprovedTxs,
- });
-
- return dispatch(updateTransaction(editingTx));
- },
-
+ resetSendState: () => dispatch(resetSendState()),
+ sign: () => dispatch(signTransaction()),
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
const hexPrefixedAddress = addHexPrefix(newAddress);
if (addressIsNew(toAccounts, hexPrefixedAddress)) {
diff --git a/ui/pages/send/send-footer/send-footer.container.test.js b/ui/pages/send/send-footer/send-footer.container.test.js
index 3cb6e474e..61c081719 100644
--- a/ui/pages/send/send-footer/send-footer.container.test.js
+++ b/ui/pages/send/send-footer/send-footer.container.test.js
@@ -1,12 +1,7 @@
import sinon from 'sinon';
-import { clearSend } from '../../../ducks/send/send.duck';
-import { signTx, signTokenTx, addToAddressBook } from '../../../store/actions';
-import {
- addressIsNew,
- constructTxParams,
- constructUpdatedTx,
-} from './send-footer.utils';
+import { addToAddressBook } from '../../../store/actions';
+import { resetSendState, signTransaction } from '../../../ducks/send';
let mapDispatchToProps;
@@ -19,32 +14,18 @@ jest.mock('react-redux', () => ({
jest.mock('../../../store/actions.js', () => ({
addToAddressBook: jest.fn(),
- signTokenTx: jest.fn(),
- signTx: jest.fn(),
- updateTransaction: jest.fn(),
}));
-jest.mock('../../../ducks/send/send.duck.js', () => ({
- clearSend: jest.fn(),
+jest.mock('../../../ducks/metamask/metamask', () => ({
+ getSendToAccounts: (s) => [`mockToAccounts:${s}`],
}));
-jest.mock('../../../selectors/send.js', () => ({
- getGasLimit: (s) => `mockGasLimit:${s}`,
+jest.mock('../../../ducks/send', () => ({
getGasPrice: (s) => `mockGasPrice:${s}`,
- getGasTotal: (s) => `mockGasTotal:${s}`,
- getSendToken: (s) => `mockSendToken:${s}`,
- getSendAmount: (s) => `mockAmount:${s}`,
- getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
- getSendFromObject: (s) => `mockFromObject:${s}`,
getSendTo: (s) => `mockTo:${s}`,
- getSendToNickname: (s) => `mockToNickname:${s}`,
- getSendToAccounts: (s) => `mockToAccounts:${s}`,
- getTokenBalance: (s) => `mockTokenBalance:${s}`,
- getSendHexData: (s) => `mockHexData:${s}`,
- getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
getSendErrors: (s) => `mockSendErrors:${s}`,
- isSendFormInError: (s) => `mockInError:${s}`,
- getDefaultActiveButtonIndex: () => 0,
+ resetSendState: jest.fn(),
+ signTransaction: jest.fn(),
}));
jest.mock('../../../selectors/custom-gas.js', () => ({
@@ -52,15 +33,6 @@ jest.mock('../../../selectors/custom-gas.js', () => ({
{ gasEstimateType: `mockGasEstimateType:${s}` },
],
}));
-
-jest.mock('./send-footer.utils', () => ({
- addressIsNew: jest.fn().mockReturnValue(true),
- constructTxParams: jest.fn().mockReturnValue({ value: 'mockAmount' }),
- constructUpdatedTx: jest
- .fn()
- .mockReturnValue('mockConstructedUpdatedTxParams'),
-}));
-
require('./send-footer.container.js');
describe('send-footer container', () => {
@@ -73,94 +45,19 @@ describe('send-footer container', () => {
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy);
});
- describe('clearSend()', () => {
+ describe('resetSendState()', () => {
it('should dispatch an action', () => {
- mapDispatchToPropsObject.clearSend();
+ mapDispatchToPropsObject.resetSendState();
expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(clearSend).toHaveBeenCalled();
+ expect(resetSendState).toHaveBeenCalled();
});
});
describe('sign()', () => {
- it('should dispatch a signTokenTx action if sendToken is defined', () => {
- mapDispatchToPropsObject.sign({
- sendToken: {
- address: '0xabc',
- },
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- });
+ it('should dispatch a signTransaction action', () => {
+ mapDispatchToPropsObject.sign();
expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(constructTxParams).toHaveBeenCalledWith({
- data: undefined,
- sendToken: {
- address: '0xabc',
- },
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- });
- expect(signTokenTx).toHaveBeenCalledWith(
- '0xabc',
- 'mockTo',
- 'mockAmount',
- { value: 'mockAmount' },
- );
- });
-
- it('should dispatch a sign action if sendToken is not defined', () => {
- mapDispatchToPropsObject.sign({
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- });
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(constructTxParams).toHaveBeenCalledWith({
- data: undefined,
- sendToken: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- });
- expect(signTx).toHaveBeenCalledWith({
- value: 'mockAmount',
- });
- });
- });
-
- describe('update()', () => {
- it('should dispatch an updateTransaction action', () => {
- mapDispatchToPropsObject.update({
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- editingTransactionId: 'mockEditingTransactionId',
- sendToken: { address: 'mockAddress' },
- unapprovedTxs: 'mockUnapprovedTxs',
- });
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(constructUpdatedTx).toHaveBeenCalledWith({
- data: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- editingTransactionId: 'mockEditingTransactionId',
- sendToken: { address: 'mockAddress' },
- unapprovedTxs: 'mockUnapprovedTxs',
- });
+ expect(signTransaction).toHaveBeenCalledTimes(1);
});
});
@@ -168,14 +65,10 @@ describe('send-footer container', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.addToAddressBookIfNew(
'mockNewAddress',
- 'mockToAccounts',
+ [{ address: 'mockToAccounts' }],
'mockNickname',
);
expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(addressIsNew).toHaveBeenCalledWith(
- 'mockToAccounts',
- '0xmockNewAddress',
- );
expect(addToAddressBook).toHaveBeenCalledWith(
'0xmockNewAddress',
'mockNickname',
diff --git a/ui/pages/send/send-footer/send-footer.utils.js b/ui/pages/send/send-footer/send-footer.utils.js
deleted file mode 100644
index 778b07867..000000000
--- a/ui/pages/send/send-footer/send-footer.utils.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import ethAbi from 'ethereumjs-abi';
-import { TOKEN_TRANSFER_FUNCTION_SIGNATURE } from '../send.constants';
-import { addHexPrefix } from '../../../../app/scripts/lib/util';
-import { addHexPrefixToObjectValues } from '../../../helpers/utils/util';
-
-export function constructTxParams({
- sendToken,
- data,
- to,
- amount,
- from,
- gas,
- gasPrice,
-}) {
- const txParams = {
- data,
- from,
- value: '0',
- gas,
- gasPrice,
- };
-
- if (!sendToken) {
- txParams.value = amount;
- txParams.to = to;
- }
-
- return addHexPrefixToObjectValues(txParams);
-}
-
-export function constructUpdatedTx({
- amount,
- data,
- editingTransactionId,
- from,
- gas,
- gasPrice,
- sendToken,
- to,
- unapprovedTxs,
-}) {
- const unapprovedTx = unapprovedTxs[editingTransactionId];
- const txParamsData = unapprovedTx.txParams.data
- ? unapprovedTx.txParams.data
- : data;
-
- const editingTx = {
- ...unapprovedTx,
- txParams: Object.assign(
- unapprovedTx.txParams,
- addHexPrefixToObjectValues({
- data: txParamsData,
- to,
- from,
- gas,
- gasPrice,
- value: amount,
- }),
- ),
- };
-
- if (sendToken) {
- Object.assign(
- editingTx.txParams,
- addHexPrefixToObjectValues({
- value: '0',
- to: sendToken.address,
- data:
- TOKEN_TRANSFER_FUNCTION_SIGNATURE +
- Array.prototype.map
- .call(
- ethAbi.rawEncode(
- ['address', 'uint256'],
- [to, addHexPrefix(amount)],
- ),
- (x) => `00${x.toString(16)}`.slice(-2),
- )
- .join(''),
- }),
- );
- }
-
- if (typeof editingTx.txParams.data === 'undefined') {
- delete editingTx.txParams.data;
- }
-
- return editingTx;
-}
-
-export function addressIsNew(toAccounts, newAddress) {
- const newAddressNormalized = newAddress.toLowerCase();
- const foundMatching = toAccounts.some(
- ({ address }) => address.toLowerCase() === newAddressNormalized,
- );
- return !foundMatching;
-}
diff --git a/ui/pages/send/send-footer/send-footer.utils.test.js b/ui/pages/send/send-footer/send-footer.utils.test.js
deleted file mode 100644
index 034ca2ecd..000000000
--- a/ui/pages/send/send-footer/send-footer.utils.test.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import { addHexPrefixToObjectValues } from '../../../helpers/utils/util';
-import { TOKEN_TRANSFER_FUNCTION_SIGNATURE } from '../send.constants';
-
-import {
- addressIsNew,
- constructTxParams,
- constructUpdatedTx,
-} from './send-footer.utils';
-
-jest.mock('ethereumjs-abi', () => ({
- rawEncode: jest.fn((arr1, arr2) => {
- return [...arr1, ...arr2];
- }),
-}));
-
-describe('send-footer utils', () => {
- describe('addHexPrefixToObjectValues()', () => {
- it('should return a new object with the same properties with a 0x prefix', () => {
- expect(
- addHexPrefixToObjectValues({
- prop1: '0x123',
- prop2: '456',
- prop3: 'x',
- }),
- ).toStrictEqual({
- prop1: '0x123',
- prop2: '0x456',
- prop3: '0xx',
- });
- });
- });
-
- describe('addressIsNew()', () => {
- it('should return false if the address exists in toAccounts', () => {
- expect(
- addressIsNew(
- [{ address: '0xabc' }, { address: '0xdef' }, { address: '0xghi' }],
- '0xdef',
- ),
- ).toStrictEqual(false);
- });
-
- it('should return true if the address does not exists in toAccounts', () => {
- expect(
- addressIsNew(
- [{ address: '0xabc' }, { address: '0xdef' }, { address: '0xghi' }],
- '0xxyz',
- ),
- ).toStrictEqual(true);
- });
- });
-
- describe('constructTxParams()', () => {
- it('should return a new txParams object with data if there data is given', () => {
- expect(
- constructTxParams({
- data: 'someData',
- sendToken: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- }),
- ).toStrictEqual({
- data: '0xsomeData',
- to: '0xmockTo',
- value: '0xmockAmount',
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- });
- });
-
- it('should return a new txParams object with value and to properties if there is no sendToken', () => {
- expect(
- constructTxParams({
- sendToken: undefined,
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- }),
- ).toStrictEqual({
- data: undefined,
- to: '0xmockTo',
- value: '0xmockAmount',
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- });
- });
-
- it('should return a new txParams object without a to property and a 0 value if there is a sendToken', () => {
- expect(
- constructTxParams({
- sendToken: { address: '0x0' },
- to: 'mockTo',
- amount: 'mockAmount',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- }),
- ).toStrictEqual({
- data: undefined,
- value: '0x0',
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- });
- });
- });
-
- describe('constructUpdatedTx()', () => {
- it('should return a new object with an updated txParams', () => {
- const result = constructUpdatedTx({
- amount: 'mockAmount',
- editingTransactionId: '0x456',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- sendToken: false,
- to: 'mockTo',
- unapprovedTxs: {
- '0x123': {},
- '0x456': {
- unapprovedTxParam: 'someOtherParam',
- txParams: {
- data: 'someData',
- },
- },
- },
- });
- expect(result).toStrictEqual({
- unapprovedTxParam: 'someOtherParam',
- txParams: {
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- value: '0xmockAmount',
- to: '0xmockTo',
- data: '0xsomeData',
- },
- });
- });
-
- it('should not have data property if there is non in the original tx', () => {
- const result = constructUpdatedTx({
- amount: 'mockAmount',
- editingTransactionId: '0x456',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- sendToken: false,
- to: 'mockTo',
- unapprovedTxs: {
- '0x123': {},
- '0x456': {
- unapprovedTxParam: 'someOtherParam',
- txParams: {
- from: 'oldFrom',
- gas: 'oldGas',
- gasPrice: 'oldGasPrice',
- },
- },
- },
- });
-
- expect(result).toStrictEqual({
- unapprovedTxParam: 'someOtherParam',
- txParams: {
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- value: '0xmockAmount',
- to: '0xmockTo',
- },
- });
- });
-
- it('should have token property values if sendToken is truthy', () => {
- const result = constructUpdatedTx({
- amount: 'mockAmount',
- editingTransactionId: '0x456',
- from: 'mockFrom',
- gas: 'mockGas',
- gasPrice: 'mockGasPrice',
- sendToken: {
- address: 'mockTokenAddress',
- },
- to: 'mockTo',
- unapprovedTxs: {
- '0x123': {},
- '0x456': {
- unapprovedTxParam: 'someOtherParam',
- txParams: {},
- },
- },
- });
-
- expect(result).toStrictEqual({
- unapprovedTxParam: 'someOtherParam',
- txParams: {
- from: '0xmockFrom',
- gas: '0xmockGas',
- gasPrice: '0xmockGasPrice',
- value: '0x0',
- to: '0xmockTokenAddress',
- data: `${TOKEN_TRANSFER_FUNCTION_SIGNATURE}ss56Tont`,
- },
- });
- });
- });
-});
diff --git a/ui/pages/send/send-header/index.js b/ui/pages/send/send-header/index.js
index cfb482303..b4bda8af7 100644
--- a/ui/pages/send/send-header/index.js
+++ b/ui/pages/send/send-header/index.js
@@ -1 +1 @@
-export { default } from './send-header.container';
+export { default } from './send-header.component';
diff --git a/ui/pages/send/send-header/send-header.component.js b/ui/pages/send/send-header/send-header.component.js
index 303ef4c7a..1b8af5312 100644
--- a/ui/pages/send/send-header/send-header.component.js
+++ b/ui/pages/send/send-header/send-header.component.js
@@ -1,33 +1,47 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
import PageContainerHeader from '../../../components/ui/page-container/page-container-header';
+import { getMostRecentOverviewPage } from '../../../ducks/history/history';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ ASSET_TYPES,
+ getSendAsset,
+ getSendStage,
+ resetSendState,
+ SEND_STAGES,
+} from '../../../ducks/send';
-export default class SendHeader extends Component {
- static propTypes = {
- clearSend: PropTypes.func,
- history: PropTypes.object,
- mostRecentOverviewPage: PropTypes.string,
- titleKey: PropTypes.string,
- };
+export default function SendHeader() {
+ const history = useHistory();
+ const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
+ const dispatch = useDispatch();
+ const stage = useSelector(getSendStage);
+ const asset = useSelector(getSendAsset);
+ const t = useI18nContext();
- static contextTypes = {
- t: PropTypes.func,
- };
-
- onClose() {
- const { clearSend, history, mostRecentOverviewPage } = this.props;
- clearSend();
+ const onClose = () => {
+ dispatch(resetSendState());
history.push(mostRecentOverviewPage);
+ };
+
+ let title = asset.type === ASSET_TYPES.NATIVE ? t('send') : t('sendTokens');
+
+ if (
+ stage === SEND_STAGES.ADD_RECIPIENT ||
+ stage === SEND_STAGES.UNINITIALIZED
+ ) {
+ title = t('addRecipient');
+ } else if (stage === SEND_STAGES.EDIT) {
+ title = t('edit');
}
- render() {
- return (
- this.onClose()}
- title={this.context.t(this.props.titleKey)}
- headerCloseText={this.context.t('cancel')}
- />
- );
- }
+ return (
+
+ );
}
diff --git a/ui/pages/send/send-header/send-header.component.test.js b/ui/pages/send/send-header/send-header.component.test.js
index 8ff76c35e..a8fa64342 100644
--- a/ui/pages/send/send-header/send-header.component.test.js
+++ b/ui/pages/send/send-header/send-header.component.test.js
@@ -1,73 +1,120 @@
import React from 'react';
-import { shallow } from 'enzyme';
-import sinon from 'sinon';
-import PageContainerHeader from '../../../components/ui/page-container/page-container-header';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
+import { fireEvent } from '@testing-library/react';
+import { ASSET_TYPES, initialState, SEND_STAGES } from '../../../ducks/send';
+import { renderWithProvider } from '../../../../test/jest';
import SendHeader from './send-header.component';
+const middleware = [thunk];
+
+jest.mock('react-router-dom', () => {
+ const original = jest.requireActual('react-router-dom');
+ return {
+ ...original,
+ useHistory: () => ({
+ push: jest.fn(),
+ }),
+ };
+});
+
describe('SendHeader Component', () => {
- let wrapper;
-
- const propsMethodSpies = {
- clearSend: sinon.spy(),
- };
- const historySpies = {
- push: sinon.spy(),
- };
-
- beforeAll(() => {
- sinon.spy(SendHeader.prototype, 'onClose');
- });
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } },
- );
- });
-
- afterEach(() => {
- propsMethodSpies.clearSend.resetHistory();
- historySpies.push.resetHistory();
- SendHeader.prototype.onClose.resetHistory();
- });
-
- afterAll(() => {
- sinon.restore();
- });
-
- describe('onClose', () => {
- it('should call clearSend', () => {
- expect(propsMethodSpies.clearSend.callCount).toStrictEqual(0);
- wrapper.instance().onClose();
- expect(propsMethodSpies.clearSend.callCount).toStrictEqual(1);
- });
-
- it('should call history.push', () => {
- expect(historySpies.push.callCount).toStrictEqual(0);
- wrapper.instance().onClose();
- expect(historySpies.push.callCount).toStrictEqual(1);
- expect(historySpies.push.getCall(0).args[0]).toStrictEqual(
- 'mostRecentOverviewPage',
+ describe('Title', () => {
+ it('should render "Add Recipient" for UNINITIALIZED or ADD_RECIPIENT stages', () => {
+ const { getByText, rerender } = renderWithProvider(
+ ,
+ configureMockStore(middleware)({
+ send: initialState,
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
);
+ expect(getByText('Add Recipient')).toBeTruthy();
+ rerender(
+ ,
+ configureMockStore(middleware)({
+ send: { ...initialState, stage: SEND_STAGES.ADD_RECIPIENT },
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
+ );
+ expect(getByText('Add Recipient')).toBeTruthy();
+ });
+
+ it('should render "Send" for DRAFT stage when asset type is NATIVE', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ configureMockStore(middleware)({
+ send: {
+ ...initialState,
+ stage: SEND_STAGES.DRAFT,
+ asset: { ...initialState.asset, type: ASSET_TYPES.NATIVE },
+ },
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
+ );
+ expect(getByText('Send')).toBeTruthy();
+ });
+
+ it('should render "Send Tokens" for DRAFT stage when asset type is TOKEN', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ configureMockStore(middleware)({
+ send: {
+ ...initialState,
+ stage: SEND_STAGES.DRAFT,
+ asset: { ...initialState.asset, type: ASSET_TYPES.TOKEN },
+ },
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
+ );
+ expect(getByText('Send Tokens')).toBeTruthy();
+ });
+
+ it('should render "Edit" for EDIT stage', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ configureMockStore(middleware)({
+ send: {
+ ...initialState,
+ stage: SEND_STAGES.EDIT,
+ },
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
+ );
+ expect(getByText('Edit')).toBeTruthy();
});
});
- describe('render', () => {
- it('should render a PageContainerHeader component', () => {
- expect(wrapper.find(PageContainerHeader)).toHaveLength(1);
+ describe('Cancel Button', () => {
+ it('has a cancel button in header', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ configureMockStore(middleware)({
+ send: initialState,
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ }),
+ );
+ expect(getByText('Cancel')).toBeTruthy();
});
- it('should pass the correct props to PageContainerHeader', () => {
- const { onClose, title } = wrapper.find(PageContainerHeader).props();
- expect(title).toStrictEqual('mockTitleKey');
- expect(SendHeader.prototype.onClose.callCount).toStrictEqual(0);
- onClose();
- expect(SendHeader.prototype.onClose.callCount).toStrictEqual(1);
+ it('resets send state when clicked', () => {
+ const store = configureMockStore(middleware)({
+ send: initialState,
+ gas: { basicEstimateStatus: 'LOADING' },
+ history: { mostRecentOverviewPage: 'activity' },
+ });
+ const { getByText } = renderWithProvider(, store);
+ const expectedActions = [
+ { type: 'send/resetSendState', payload: undefined },
+ ];
+ fireEvent.click(getByText('Cancel'));
+ expect(store.getActions()).toStrictEqual(expectedActions);
});
});
});
diff --git a/ui/pages/send/send-header/send-header.container.js b/ui/pages/send/send-header/send-header.container.js
deleted file mode 100644
index b66a9ba89..000000000
--- a/ui/pages/send/send-header/send-header.container.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { connect } from 'react-redux';
-import { clearSend } from '../../../ducks/send/send.duck';
-import { getTitleKey } from '../../../selectors';
-import { getMostRecentOverviewPage } from '../../../ducks/history/history';
-import SendHeader from './send-header.component';
-
-export default connect(mapStateToProps, mapDispatchToProps)(SendHeader);
-
-function mapStateToProps(state) {
- return {
- mostRecentOverviewPage: getMostRecentOverviewPage(state),
- titleKey: getTitleKey(state),
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- clearSend: () => dispatch(clearSend()),
- };
-}
diff --git a/ui/pages/send/send.component.js b/ui/pages/send/send.component.js
deleted file mode 100644
index 6954f9cfc..000000000
--- a/ui/pages/send/send.component.js
+++ /dev/null
@@ -1,403 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { debounce } from 'lodash';
-import { isValidHexAddress } from '../../../shared/modules/hexstring-utils';
-import {
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
- doesAmountErrorRequireUpdate,
-} from './send.utils';
-import {
- getToWarningObject,
- getToErrorObject,
-} from './send-content/add-recipient/add-recipient';
-import SendHeader from './send-header';
-import AddRecipient from './send-content/add-recipient';
-import SendContent from './send-content';
-import SendFooter from './send-footer';
-import EnsInput from './send-content/add-recipient/ens-input';
-import {
- INVALID_RECIPIENT_ADDRESS_ERROR,
- KNOWN_RECIPIENT_ADDRESS_ERROR,
- CONTRACT_ADDRESS_ERROR,
-} from './send.constants';
-
-export default class SendTransactionScreen extends Component {
- static propTypes = {
- addressBook: PropTypes.arrayOf(PropTypes.object),
- amount: PropTypes.string,
- blockGasLimit: PropTypes.string,
- conversionRate: PropTypes.number,
- editingTransactionId: PropTypes.string,
- fetchBasicGasEstimates: PropTypes.func.isRequired,
- from: PropTypes.object,
- gasLimit: PropTypes.string,
- gasPrice: PropTypes.string,
- gasTotal: PropTypes.string,
- history: PropTypes.object,
- chainId: PropTypes.string,
- primaryCurrency: PropTypes.string,
- resetSendState: PropTypes.func.isRequired,
- selectedAddress: PropTypes.string,
- sendToken: PropTypes.object,
- showHexData: PropTypes.bool,
- to: PropTypes.string,
- toNickname: PropTypes.string,
- tokens: PropTypes.array,
- tokenBalance: PropTypes.string,
- tokenContract: PropTypes.object,
- updateAndSetGasLimit: PropTypes.func.isRequired,
- updateSendEnsResolution: PropTypes.func.isRequired,
- updateSendEnsResolutionError: PropTypes.func.isRequired,
- updateSendErrors: PropTypes.func.isRequired,
- updateSendTo: PropTypes.func.isRequired,
- updateSendTokenBalance: PropTypes.func.isRequired,
- updateToNicknameIfNecessary: PropTypes.func.isRequired,
- scanQrCode: PropTypes.func.isRequired,
- qrCodeDetected: PropTypes.func.isRequired,
- qrCodeData: PropTypes.object,
- sendTokenAddress: PropTypes.string,
- gasIsExcessive: PropTypes.bool.isRequired,
- };
-
- static contextTypes = {
- t: PropTypes.func,
- metricsEvent: PropTypes.func,
- };
-
- state = {
- query: '',
- toError: null,
- toWarning: null,
- internalSearch: false,
- };
-
- constructor(props) {
- super(props);
- this.dValidate = debounce(this.validate, 1000);
- }
-
- componentDidUpdate(prevProps) {
- const {
- amount,
- conversionRate,
- from: { address, balance },
- gasTotal,
- chainId,
- primaryCurrency,
- sendToken,
- tokenBalance,
- updateSendErrors,
- updateSendTo,
- updateSendTokenBalance,
- tokenContract,
- to,
- toNickname,
- addressBook,
- updateToNicknameIfNecessary,
- qrCodeData,
- qrCodeDetected,
- } = this.props;
- const { toError, toWarning } = this.state;
-
- let updateGas = false;
- const {
- from: { balance: prevBalance },
- gasTotal: prevGasTotal,
- tokenBalance: prevTokenBalance,
- chainId: prevChainId,
- sendToken: prevSendToken,
- to: prevTo,
- } = prevProps;
-
- const uninitialized = [prevBalance, prevGasTotal].every((n) => n === null);
-
- const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({
- balance,
- gasTotal,
- prevBalance,
- prevGasTotal,
- prevTokenBalance,
- sendToken,
- tokenBalance,
- });
-
- if (amountErrorRequiresUpdate) {
- const amountErrorObject = getAmountErrorObject({
- amount,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- sendToken,
- tokenBalance,
- });
- const gasFeeErrorObject = sendToken
- ? getGasFeeErrorObject({
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- sendToken,
- })
- : { gasFee: null };
- updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject));
- }
-
- if (!uninitialized) {
- if (chainId !== prevChainId && chainId !== undefined) {
- updateSendTokenBalance({
- sendToken,
- tokenContract,
- address,
- });
- updateToNicknameIfNecessary(to, toNickname, addressBook);
- this.props.fetchBasicGasEstimates();
- updateGas = true;
- }
- }
-
- const prevTokenAddress = prevSendToken && prevSendToken.address;
- const sendTokenAddress = sendToken && sendToken.address;
-
- if (sendTokenAddress && prevTokenAddress !== sendTokenAddress) {
- this.updateSendToken();
- this.validate(this.state.query);
- updateGas = true;
- }
-
- let scannedAddress;
- if (qrCodeData) {
- if (qrCodeData.type === 'address') {
- scannedAddress = qrCodeData.values.address.toLowerCase();
- if (isValidHexAddress(scannedAddress, { allowNonPrefixed: false })) {
- const currentAddress = prevTo?.toLowerCase();
- if (currentAddress !== scannedAddress) {
- updateSendTo(scannedAddress);
- updateGas = true;
- // Clean up QR code data after handling
- qrCodeDetected(null);
- }
- } else {
- scannedAddress = null;
- qrCodeDetected(null);
- this.setState({ toError: INVALID_RECIPIENT_ADDRESS_ERROR });
- }
- }
- }
-
- if (updateGas) {
- if (scannedAddress) {
- this.updateGas({ to: scannedAddress });
- } else {
- this.updateGas();
- }
- }
-
- // If selecting ETH after selecting a token, clear token related messages.
- if (prevSendToken && !sendToken) {
- let error = toError;
- let warning = toWarning;
-
- if (toError === CONTRACT_ADDRESS_ERROR) {
- error = null;
- }
-
- if (toWarning === KNOWN_RECIPIENT_ADDRESS_ERROR) {
- warning = null;
- }
-
- this.setState({
- toError: error,
- toWarning: warning,
- });
- }
- }
-
- componentDidMount() {
- this.props.fetchBasicGasEstimates().then(() => {
- this.updateGas();
- });
- }
-
- UNSAFE_componentWillMount() {
- this.updateSendToken();
-
- // Show QR Scanner modal if ?scan=true
- if (window.location.search === '?scan=true') {
- this.props.scanQrCode();
-
- // Clear the queryString param after showing the modal
- const cleanUrl = window.location.href.split('?')[0];
- window.history.pushState({}, null, `${cleanUrl}`);
- window.location.hash = '#send';
- }
- }
-
- componentWillUnmount() {
- this.props.resetSendState();
- }
-
- onRecipientInputChange = (query) => {
- const { internalSearch } = this.state;
-
- if (!internalSearch) {
- if (query) {
- this.dValidate(query);
- } else {
- this.dValidate.cancel();
- this.validate(query);
- }
- }
-
- this.setState({ query });
- };
-
- setInternalSearch(internalSearch) {
- this.setState({ query: '', internalSearch });
- }
-
- validate(query) {
- const { tokens, sendToken, chainId, sendTokenAddress } = this.props;
-
- const { internalSearch } = this.state;
-
- if (!query || internalSearch) {
- this.setState({ toError: '', toWarning: '' });
- return;
- }
-
- const toErrorObject = getToErrorObject(query, sendTokenAddress, chainId);
- const toWarningObject = getToWarningObject(query, tokens, sendToken);
-
- this.setState({
- toError: toErrorObject.to,
- toWarning: toWarningObject.to,
- });
- }
-
- updateSendToken() {
- const {
- from: { address },
- sendToken,
- tokenContract,
- updateSendTokenBalance,
- } = this.props;
-
- updateSendTokenBalance({
- sendToken,
- tokenContract,
- address,
- });
- }
-
- updateGas({ to: updatedToAddress, amount: value, data } = {}) {
- const {
- amount,
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- selectedAddress,
- sendToken,
- to: currentToAddress,
- updateAndSetGasLimit,
- } = this.props;
-
- updateAndSetGasLimit({
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- selectedAddress,
- sendToken,
- to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
- value: value || amount,
- data,
- });
- }
-
- render() {
- const { history, to } = this.props;
- let content;
-
- if (to) {
- content = this.renderSendContent();
- } else {
- content = this.renderAddRecipient();
- }
-
- return (
-
-
- {this.renderInput()}
- {content}
-
- );
- }
-
- renderInput() {
- const { internalSearch } = this.state;
- return (
- {
- this.context.metricsEvent({
- eventOpts: {
- category: 'Transactions',
- action: 'Edit Screen',
- name: 'Used QR scanner',
- },
- });
- this.props.scanQrCode();
- }}
- onChange={this.onRecipientInputChange}
- onValidAddressTyped={(address) => this.props.updateSendTo(address, '')}
- onPaste={(text) => {
- this.props.updateSendTo(text) && this.updateGas();
- }}
- onReset={() => this.props.updateSendTo('', '')}
- updateEnsResolution={this.props.updateSendEnsResolution}
- updateEnsResolutionError={this.props.updateSendEnsResolutionError}
- internalSearch={internalSearch}
- />
- );
- }
-
- renderAddRecipient() {
- const { toError, toWarning } = this.state;
- return (
-
- this.updateGas({ to, amount, data })
- }
- query={this.state.query}
- toError={toError}
- toWarning={toWarning}
- setInternalSearch={(internalSearch) =>
- this.setInternalSearch(internalSearch)
- }
- />
- );
- }
-
- renderSendContent() {
- const { history, showHexData, gasIsExcessive } = this.props;
- const { toWarning, toError } = this.state;
-
- return [
-
- this.updateGas({ to, amount, data })
- }
- showHexData={showHexData}
- warning={toWarning}
- error={toError}
- gasIsExcessive={gasIsExcessive}
- />,
- ,
- ];
- }
-}
diff --git a/ui/pages/send/send.component.test.js b/ui/pages/send/send.component.test.js
deleted file mode 100644
index 5cc90307e..000000000
--- a/ui/pages/send/send.component.test.js
+++ /dev/null
@@ -1,467 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import sinon from 'sinon';
-import {
- RINKEBY_CHAIN_ID,
- ROPSTEN_CHAIN_ID,
-} from '../../../shared/constants/network';
-import SendTransactionScreen from './send.component';
-import * as util from './send.utils';
-
-import SendHeader from './send-header/send-header.container';
-import SendContent from './send-content/send-content.container';
-import SendFooter from './send-footer/send-footer.container';
-
-import AddRecipient from './send-content/add-recipient/add-recipient.container';
-
-jest.mock('./send.utils', () => ({
- getToAddressForGasUpdate: jest.fn().mockReturnValue('mockAddress'),
- getAmountErrorObject: jest.fn().mockReturnValue({
- amount: 'mockAmountError',
- }),
- getGasFeeErrorObject: jest.fn().mockReturnValue({
- gasFee: 'mockGasFeeError',
- }),
- doesAmountErrorRequireUpdate: jest.fn(
- (obj) => obj.balance !== obj.prevBalance,
- ),
-}));
-
-describe('Send Component', () => {
- let wrapper, didMountSpy, updateGasSpy;
-
- const mockBasicGasEstimates = {
- blockTime: 'mockBlockTime',
- };
-
- const propsMethodSpies = {
- updateAndSetGasLimit: jest.fn(),
- updateSendErrors: jest.fn(),
- updateSendTokenBalance: jest.fn(),
- resetSendState: jest.fn(),
- fetchBasicGasEstimates: jest.fn(() =>
- Promise.resolve(mockBasicGasEstimates),
- ),
- fetchGasEstimates: jest.fn(),
- updateToNicknameIfNecessary: jest.fn(),
- };
-
- beforeAll(() => {
- didMountSpy = sinon.spy(
- SendTransactionScreen.prototype,
- 'componentDidMount',
- );
- updateGasSpy = sinon.spy(SendTransactionScreen.prototype, 'updateGas');
- });
-
- beforeEach(() => {
- wrapper = shallow(
- undefined}
- scanQrCode={() => undefined}
- updateSendEnsResolution={() => undefined}
- updateSendEnsResolutionError={() => undefined}
- updateSendErrors={propsMethodSpies.updateSendErrors}
- updateSendTo={() => undefined}
- updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
- resetSendState={propsMethodSpies.resetSendState}
- updateToNicknameIfNecessary={
- propsMethodSpies.updateToNicknameIfNecessary
- }
- gasIsExcessive={false}
- />,
- );
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- didMountSpy.resetHistory();
- updateGasSpy.resetHistory();
- });
-
- describe('componentDidMount', () => {
- it('should call componentDidMount', () => {
- expect(didMountSpy.callCount).toStrictEqual(1);
- });
-
- it('should call props.fetchBasicGasAndTimeEstimates', () => {
- propsMethodSpies.fetchBasicGasEstimates.mockClear();
- expect(propsMethodSpies.fetchBasicGasEstimates).not.toHaveBeenCalled();
- wrapper.instance().componentDidMount();
- expect(propsMethodSpies.fetchBasicGasEstimates).toHaveBeenCalled();
- });
-
- it('should call this.updateGas', () => {
- expect(updateGasSpy.callCount).toStrictEqual(1);
- });
- });
-
- describe('componentWillUnmount', () => {
- it('should call this.props.resetSendState', () => {
- propsMethodSpies.resetSendState.mockClear();
- expect(propsMethodSpies.resetSendState).not.toHaveBeenCalled();
- wrapper.instance().componentWillUnmount();
- expect(propsMethodSpies.resetSendState).toHaveBeenCalled();
- });
- });
-
- describe('componentDidUpdate', () => {
- it('should call doesAmountErrorRequireUpdate with the expected params', () => {
- wrapper.instance().componentDidUpdate({
- from: {
- balance: '',
- },
- });
- expect(util.doesAmountErrorRequireUpdate).toHaveBeenCalled();
- expect(util.doesAmountErrorRequireUpdate.mock.calls[0][0]).toMatchObject({
- balance: 'mockBalance',
- gasTotal: 'mockGasTotal',
- prevBalance: '',
- prevGasTotal: undefined,
- prevTokenBalance: undefined,
- sendToken: {
- address: 'mockTokenAddress',
- decimals: 18,
- symbol: 'TST',
- },
- tokenBalance: 'mockTokenBalance',
- });
- });
-
- it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => {
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'mockBalance',
- },
- });
- expect(util.getAmountErrorObject).not.toHaveBeenCalled();
- });
-
- it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => {
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- });
- expect(util.getAmountErrorObject).toHaveBeenCalled();
- expect(util.getAmountErrorObject.mock.calls[0][0]).toMatchObject({
- amount: 'mockAmount',
- balance: 'mockBalance',
- conversionRate: 10,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- sendToken: {
- address: 'mockTokenAddress',
- decimals: 18,
- symbol: 'TST',
- },
- tokenBalance: 'mockTokenBalance',
- });
- });
-
- it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and sendToken is truthy', () => {
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- });
- expect(util.getGasFeeErrorObject).toHaveBeenCalled();
- expect(util.getGasFeeErrorObject.mock.calls[0][0]).toMatchObject({
- balance: 'mockBalance',
- conversionRate: 10,
- gasTotal: 'mockGasTotal',
- primaryCurrency: 'mockPrimaryCurrency',
- sendToken: {
- address: 'mockTokenAddress',
- decimals: 18,
- symbol: 'TST',
- },
- });
- });
-
- it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => {
- wrapper.instance().componentDidUpdate({
- from: { address: 'mockAddress', balance: 'mockBalance' },
- });
- expect(util.getGasFeeErrorObject).not.toHaveBeenCalled();
- });
-
- it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but sendToken is falsy', () => {
- wrapper.setProps({ sendToken: null });
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- });
- expect(util.getGasFeeErrorObject).not.toHaveBeenCalled();
- });
-
- it('should call updateSendErrors with the expected params if sendToken is falsy', () => {
- wrapper.setProps({ sendToken: null });
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- });
- expect(propsMethodSpies.updateSendErrors).toHaveBeenCalledTimes(1);
- expect(propsMethodSpies.updateSendErrors.mock.calls[0][0]).toMatchObject({
- amount: 'mockAmountError',
- gasFee: null,
- });
- });
-
- it('should call updateSendErrors with the expected params if sendToken is truthy', () => {
- wrapper.setProps({
- sendToken: { address: 'mockTokenAddress', decimals: 18, symbol: 'TST' },
- });
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- });
- expect(propsMethodSpies.updateSendErrors).toHaveBeenCalled();
- expect(propsMethodSpies.updateSendErrors.mock.calls[0][0]).toMatchObject({
- amount: 'mockAmountError',
- gasFee: 'mockGasFeeError',
- });
- });
-
- it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => {
- propsMethodSpies.updateSendTokenBalance.mockClear();
- updateGasSpy.resetHistory();
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- chainId: ROPSTEN_CHAIN_ID,
- sendToken: { address: 'mockTokenAddress', decimals: 18, symbol: 'TST' }, // Make sure not to hit updateGas when changing asset
- });
- expect(propsMethodSpies.updateSendTokenBalance).not.toHaveBeenCalled();
- expect(updateGasSpy.callCount).toStrictEqual(0);
- });
-
- it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => {
- propsMethodSpies.updateSendTokenBalance.mockClear();
- updateGasSpy.resetHistory();
- wrapper.setProps({ network: 'loading' });
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- chainId: ROPSTEN_CHAIN_ID,
- sendToken: { address: 'mockTokenAddress', decimals: 18, symbol: 'TST' }, // Make sure not to hit updateGas when changing asset
- });
- expect(propsMethodSpies.updateSendTokenBalance).not.toHaveBeenCalled();
- expect(updateGasSpy.callCount).toStrictEqual(0);
- });
-
- it('should call updateSendTokenBalance and this.updateGas with the correct params', () => {
- updateGasSpy.resetHistory();
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balanceChanged',
- },
- chainId: RINKEBY_CHAIN_ID,
- sendToken: { address: 'mockTokenAddress', decimals: 18, symbol: 'TST' }, // Make sure not to hit updateGas when changing asset
- });
- expect(propsMethodSpies.updateSendTokenBalance).toHaveBeenCalled();
- expect(
- propsMethodSpies.updateSendTokenBalance.mock.calls[0][0],
- ).toMatchObject({
- sendToken: {
- address: 'mockTokenAddress',
- decimals: 18,
- symbol: 'TST',
- }, // Make sure not to hit updateGas when changing asset
- tokenContract: { method: 'mockTokenMethod' },
- address: 'mockAddress',
- });
- expect(updateGasSpy.callCount).toStrictEqual(1);
- });
-
- it('should call updateGas when sendToken.address is changed', () => {
- wrapper.instance().componentDidUpdate({
- from: {
- balance: 'balancedChanged',
- },
- chainId: ROPSTEN_CHAIN_ID, // Make sure not to hit updateGas when changing network
- sendToken: { address: 'newSelectedToken' },
- });
- expect(
- propsMethodSpies.updateToNicknameIfNecessary,
- ).not.toHaveBeenCalled(); // Network did not change
- expect(propsMethodSpies.updateAndSetGasLimit).toHaveBeenCalled();
- });
- });
-
- describe('updateGas', () => {
- it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => {
- propsMethodSpies.updateAndSetGasLimit.mockClear();
- wrapper.instance().updateGas();
- expect(propsMethodSpies.updateAndSetGasLimit).toHaveBeenCalled();
- expect(
- propsMethodSpies.updateAndSetGasLimit.mock.calls[0][0],
- ).toMatchObject({
- blockGasLimit: 'mockBlockGasLimit',
- editingTransactionId: 'mockEditingTransactionId',
- gasLimit: 'mockGasLimit',
- gasPrice: 'mockGasPrice',
- selectedAddress: 'mockSelectedAddress',
- sendToken: {
- address: 'mockTokenAddress',
- decimals: 18,
- symbol: 'TST',
- },
- to: 'mockAddress',
- value: 'mockAmount',
- data: undefined,
- });
- });
- });
-
- describe('render', () => {
- it('should render a page-container class', () => {
- expect(wrapper.find('.page-container')).toHaveLength(1);
- });
-
- it('should render SendHeader and AddRecipient', () => {
- expect(wrapper.find(SendHeader)).toHaveLength(1);
- expect(wrapper.find(AddRecipient)).toHaveLength(1);
- });
-
- it('should pass the history prop to SendHeader and SendFooter', () => {
- wrapper.setProps({
- to: '0x80F061544cC398520615B5d3e7A3BedD70cd4510',
- });
- expect(wrapper.find(SendHeader)).toHaveLength(1);
- expect(wrapper.find(SendContent)).toHaveLength(1);
- expect(wrapper.find(SendFooter)).toHaveLength(1);
- expect(wrapper.find(SendFooter).props()).toStrictEqual({
- history: { mockProp: 'history-abc' },
- });
- });
-
- it('should pass showHexData to SendContent', () => {
- wrapper.setProps({
- to: '0x80F061544cC398520615B5d3e7A3BedD70cd4510',
- });
- expect(wrapper.find(SendContent).props().showHexData).toStrictEqual(true);
- });
- });
-
- describe('validate when input change', () => {
- let clock;
-
- beforeEach(() => {
- clock = sinon.useFakeTimers();
- });
-
- afterEach(() => {
- clock.restore();
- });
-
- it('should validate when input changes', () => {
- const instance = wrapper.instance();
- instance.onRecipientInputChange(
- '0x80F061544cC398520615B5d3e7A3BedD70cd4510',
- );
-
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '0x80F061544cC398520615B5d3e7A3BedD70cd4510',
- toError: null,
- toWarning: null,
- });
- });
-
- it('should validate when input changes and has error', () => {
- const instance = wrapper.instance();
- instance.onRecipientInputChange(
- '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- );
-
- clock.tick(1001);
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- toError: 'invalidAddressRecipient',
- toWarning: null,
- });
- });
-
- it('should validate when input changes and has error on a bad network', () => {
- wrapper.setProps({ network: 'bad' });
- const instance = wrapper.instance();
- instance.onRecipientInputChange(
- '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- );
-
- clock.tick(1001);
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- toError: 'invalidAddressRecipient',
- toWarning: null,
- });
- });
-
- it('should synchronously validate when input changes to ""', () => {
- wrapper.setProps({ network: 'bad' });
- const instance = wrapper.instance();
- instance.onRecipientInputChange(
- '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- );
-
- clock.tick(1001);
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '0x80F061544cC398520615B5d3e7a3BedD70cd4510',
- toError: 'invalidAddressRecipient',
- toWarning: null,
- });
-
- instance.onRecipientInputChange('');
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '',
- toError: '',
- toWarning: '',
- });
- });
-
- it('should warn when send to a known token contract address', () => {
- wrapper.setProps({ address: '0x888', decimals: 18, symbol: '888' });
- const instance = wrapper.instance();
- instance.onRecipientInputChange(
- '0x13cb85823f78Cff38f0B0E90D3e975b8CB3AAd64',
- );
-
- clock.tick(1001);
- expect(instance.state).toStrictEqual({
- internalSearch: false,
- query: '0x13cb85823f78Cff38f0B0E90D3e975b8CB3AAd64',
- toError: null,
- toWarning: 'knownAddressRecipient',
- });
- });
- });
-});
diff --git a/ui/pages/send/send.constants.js b/ui/pages/send/send.constants.js
index ba5113603..48e96ef95 100644
--- a/ui/pages/send/send.constants.js
+++ b/ui/pages/send/send.constants.js
@@ -34,17 +34,29 @@ const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient';
const INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR =
'invalidAddressRecipientNotEthNetwork';
const REQUIRED_ERROR = 'required';
-const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient';
+const KNOWN_RECIPIENT_ADDRESS_WARNING = 'knownAddressRecipient';
const CONTRACT_ADDRESS_ERROR = 'contractAddressError';
const CONFUSING_ENS_ERROR = 'confusingEnsDomain';
+const ENS_NO_ADDRESS_FOR_NAME = 'noAddressForName';
+const ENS_NOT_FOUND_ON_NETWORK = 'ensNotFoundOnCurrentNetwork';
+const ENS_NOT_SUPPORTED_ON_NETWORK = 'ensNotSupportedOnNetwork';
+const ENS_ILLEGAL_CHARACTER = 'ensIllegalCharacter';
+const ENS_UNKNOWN_ERROR = 'ensUnknownError';
+const ENS_REGISTRATION_ERROR = 'ensRegistrationError';
export {
INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR,
- KNOWN_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_RECIPIENT_ADDRESS_WARNING,
CONTRACT_ADDRESS_ERROR,
INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
+ ENS_NO_ADDRESS_FOR_NAME,
+ ENS_NOT_FOUND_ON_NETWORK,
+ ENS_NOT_SUPPORTED_ON_NETWORK,
+ ENS_ILLEGAL_CHARACTER,
+ ENS_UNKNOWN_ERROR,
+ ENS_REGISTRATION_ERROR,
MIN_GAS_LIMIT_DEC,
MIN_GAS_LIMIT_HEX,
MIN_GAS_PRICE_DEC,
diff --git a/ui/pages/send/send.container.js b/ui/pages/send/send.container.js
deleted file mode 100644
index f942131dd..000000000
--- a/ui/pages/send/send.container.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
-import { compose } from 'redux';
-
-import {
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getPrimaryCurrency,
- getSendToken,
- getSendTokenContract,
- getSendAmount,
- getSendEditingTransactionId,
- getSendFromObject,
- getSendTo,
- getSendToNickname,
- getTokenBalance,
- getQrCodeData,
- getSelectedAddress,
- getAddressBook,
- getSendTokenAddress,
- isCustomPriceExcessive,
- getCurrentChainId,
-} from '../../selectors';
-
-import { showQrScanner, qrCodeDetected } from '../../store/actions';
-import {
- resetSendState,
- updateSendErrors,
- updateSendTo,
- updateSendTokenBalance,
- updateGasData,
- setGasTotal,
- updateSendEnsResolution,
- updateSendEnsResolutionError,
-} from '../../ducks/send/send.duck';
-import { fetchBasicGasEstimates } from '../../ducks/gas/gas.duck';
-import {
- getBlockGasLimit,
- getConversionRate,
- getSendHexDataFeatureFlagState,
- getTokens,
-} from '../../ducks/metamask/metamask';
-import { isValidDomainName } from '../../helpers/utils/util';
-import { calcGasTotal } from './send.utils';
-import SendEther from './send.component';
-
-function mapStateToProps(state) {
- const editingTransactionId = getSendEditingTransactionId(state);
-
- return {
- addressBook: getAddressBook(state),
- amount: getSendAmount(state),
- blockGasLimit: getBlockGasLimit(state),
- conversionRate: getConversionRate(state),
- editingTransactionId,
- from: getSendFromObject(state),
- gasLimit: getGasLimit(state),
- gasPrice: getGasPrice(state),
- gasTotal: getGasTotal(state),
- chainId: getCurrentChainId(state),
- primaryCurrency: getPrimaryCurrency(state),
- qrCodeData: getQrCodeData(state),
- selectedAddress: getSelectedAddress(state),
- sendToken: getSendToken(state),
- showHexData: getSendHexDataFeatureFlagState(state),
- to: getSendTo(state),
- toNickname: getSendToNickname(state),
- tokens: getTokens(state),
- tokenBalance: getTokenBalance(state),
- tokenContract: getSendTokenContract(state),
- sendTokenAddress: getSendTokenAddress(state),
- gasIsExcessive: isCustomPriceExcessive(state, true),
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- updateAndSetGasLimit: ({
- blockGasLimit,
- editingTransactionId,
- gasLimit,
- gasPrice,
- selectedAddress,
- sendToken,
- to,
- value,
- data,
- }) => {
- editingTransactionId
- ? dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
- : dispatch(
- updateGasData({
- gasPrice,
- selectedAddress,
- sendToken,
- blockGasLimit,
- to,
- value,
- data,
- }),
- );
- },
- updateSendTokenBalance: ({ sendToken, tokenContract, address }) => {
- dispatch(
- updateSendTokenBalance({
- sendToken,
- tokenContract,
- address,
- }),
- );
- },
- updateSendErrors: (newError) => dispatch(updateSendErrors(newError)),
- resetSendState: () => dispatch(resetSendState()),
- scanQrCode: () => dispatch(showQrScanner()),
- qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()),
- updateSendEnsResolution: (ensResolution) =>
- dispatch(updateSendEnsResolution(ensResolution)),
- updateSendEnsResolutionError: (message) =>
- dispatch(updateSendEnsResolutionError(message)),
- updateToNicknameIfNecessary: (to, toNickname, addressBook) => {
- if (isValidDomainName(toNickname)) {
- const addressBookEntry =
- addressBook.find(({ address }) => to === address) || {};
- if (!addressBookEntry.name !== toNickname) {
- dispatch(updateSendTo(to, addressBookEntry.name || ''));
- }
- }
- },
- };
-}
-
-export default compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps),
-)(SendEther);
diff --git a/ui/pages/send/send.container.test.js b/ui/pages/send/send.container.test.js
deleted file mode 100644
index 3072b3243..000000000
--- a/ui/pages/send/send.container.test.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import sinon from 'sinon';
-
-import {
- updateSendTokenBalance,
- updateGasData,
- setGasTotal,
- updateSendErrors,
- resetSendState,
-} from '../../ducks/send/send.duck';
-
-let mapDispatchToProps;
-
-jest.mock('react-redux', () => ({
- connect: (_, md) => {
- mapDispatchToProps = md;
- return () => ({});
- },
-}));
-
-jest.mock('react-router-dom', () => ({
- withRouter: () => undefined,
-}));
-
-jest.mock('redux', () => ({
- compose: (_, arg2) => () => arg2(),
-}));
-
-jest.mock('../../ducks/send/send.duck', () => ({
- updateSendErrors: jest.fn(),
- resetSendState: jest.fn(),
- updateSendTokenBalance: jest.fn(),
- updateGasData: jest.fn(),
- setGasTotal: jest.fn(),
-}));
-
-jest.mock('./send.utils.js', () => ({
- calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
-}));
-
-require('./send.container.js');
-
-describe('send container', () => {
- describe('mapDispatchToProps()', () => {
- let dispatchSpy;
- let mapDispatchToPropsObject;
-
- beforeEach(() => {
- dispatchSpy = sinon.spy();
- mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy);
- });
-
- describe('updateAndSetGasLimit()', () => {
- const mockProps = {
- blockGasLimit: 'mockBlockGasLimit',
- editingTransactionId: '0x2',
- gasLimit: '0x3',
- gasPrice: '0x4',
- selectedAddress: '0x4',
- sendToken: { address: '0x1' },
- to: 'mockTo',
- value: 'mockValue',
- data: undefined,
- };
-
- it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
- mapDispatchToPropsObject.updateAndSetGasLimit(mockProps);
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(setGasTotal).toHaveBeenCalledWith('0x30x4');
- });
-
- it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
- const {
- gasPrice,
- selectedAddress,
- sendToken,
- blockGasLimit,
- to,
- value,
- data,
- } = mockProps;
- mapDispatchToPropsObject.updateAndSetGasLimit({
- ...mockProps,
- editingTransactionId: false,
- });
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(updateGasData).toHaveBeenCalledWith({
- gasPrice,
- selectedAddress,
- sendToken,
- blockGasLimit,
- to,
- value,
- data,
- });
- });
- });
-
- describe('updateSendTokenBalance()', () => {
- const mockProps = {
- address: '0x10',
- tokenContract: '0x00a',
- sendToken: { address: '0x1' },
- };
-
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendTokenBalance({ ...mockProps });
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(updateSendTokenBalance).toHaveBeenCalledWith(mockProps);
- });
- });
-
- describe('updateSendErrors()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.updateSendErrors('mockError');
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(updateSendErrors).toHaveBeenCalledWith('mockError');
- });
- });
-
- describe('resetSendState()', () => {
- it('should dispatch an action', () => {
- mapDispatchToPropsObject.resetSendState();
- expect(dispatchSpy.calledOnce).toStrictEqual(true);
- expect(resetSendState).toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/ui/pages/send/send.js b/ui/pages/send/send.js
new file mode 100644
index 000000000..1e908d9de
--- /dev/null
+++ b/ui/pages/send/send.js
@@ -0,0 +1,112 @@
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory, useLocation } from 'react-router-dom';
+import {
+ getIsUsingMyAccountForRecipientSearch,
+ getRecipient,
+ getRecipientUserInput,
+ getSendStage,
+ initializeSendState,
+ resetRecipientInput,
+ resetSendState,
+ SEND_STAGES,
+ updateRecipient,
+ updateRecipientUserInput,
+} from '../../ducks/send';
+import { getCurrentChainId, isCustomPriceExcessive } from '../../selectors';
+import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask';
+import { showQrScanner } from '../../store/actions';
+import { useMetricEvent } from '../../hooks/useMetricEvent';
+import SendHeader from './send-header';
+import AddRecipient from './send-content/add-recipient';
+import SendContent from './send-content';
+import SendFooter from './send-footer';
+import EnsInput from './send-content/add-recipient/ens-input';
+
+const sendSliceIsCustomPriceExcessive = (state) =>
+ isCustomPriceExcessive(state, true);
+
+export default function SendTransactionScreen() {
+ const history = useHistory();
+ const chainId = useSelector(getCurrentChainId);
+ const stage = useSelector(getSendStage);
+ const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive);
+ const isUsingMyAccountsForRecipientSearch = useSelector(
+ getIsUsingMyAccountForRecipientSearch,
+ );
+ const recipient = useSelector(getRecipient);
+ const showHexData = useSelector(getSendHexDataFeatureFlagState);
+ const userInput = useSelector(getRecipientUserInput);
+ const location = useLocation();
+ const trackUsedQRScanner = useMetricEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Used QR scanner',
+ },
+ });
+
+ const dispatch = useDispatch();
+ useEffect(() => {
+ if (chainId !== undefined) {
+ dispatch(initializeSendState());
+ }
+ }, [chainId, dispatch]);
+
+ useEffect(() => {
+ if (location.search === '?scan=true') {
+ dispatch(showQrScanner());
+
+ // Clear the queryString param after showing the modal
+ const cleanUrl = window.location.href.split('?')[0];
+ window.history.pushState({}, null, `${cleanUrl}`);
+ window.location.hash = '#send';
+ }
+ }, [location, dispatch]);
+
+ useEffect(() => {
+ return () => {
+ dispatch(resetSendState());
+ };
+ }, [dispatch]);
+
+ let content;
+
+ if ([SEND_STAGES.EDIT, SEND_STAGES.DRAFT].includes(stage)) {
+ content = (
+ <>
+
+
+ >
+ );
+ } else {
+ content = ;
+ }
+
+ return (
+
+
+ dispatch(updateRecipientUserInput(address))}
+ onValidAddressTyped={(address) =>
+ dispatch(updateRecipient({ address, nickname: '' }))
+ }
+ internalSearch={isUsingMyAccountsForRecipientSearch}
+ selectedAddress={recipient.address}
+ selectedName={recipient.nickname}
+ onPaste={(text) => updateRecipient({ address: text, nickname: '' })}
+ onReset={() => dispatch(resetRecipientInput())}
+ scanQrCode={() => {
+ trackUsedQRScanner();
+ dispatch(showQrScanner());
+ }}
+ />
+ {content}
+
+ );
+}
diff --git a/ui/pages/send/send.test.js b/ui/pages/send/send.test.js
new file mode 100644
index 000000000..07bef5b26
--- /dev/null
+++ b/ui/pages/send/send.test.js
@@ -0,0 +1,173 @@
+import React from 'react';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
+import { useLocation } from 'react-router-dom';
+import { describe } from 'globalthis/implementation';
+import { initialState, SEND_STAGES } from '../../ducks/send';
+import { ensInitialState } from '../../ducks/ens';
+import { renderWithProvider } from '../../../test/jest';
+import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
+import Send from './send';
+
+const middleware = [thunk];
+
+jest.mock('react-router-dom', () => {
+ const original = jest.requireActual('react-router-dom');
+ return {
+ ...original,
+ useLocation: jest.fn(() => ({ search: '' })),
+ useHistory: () => ({
+ push: jest.fn(),
+ }),
+ };
+});
+
+jest.mock(
+ 'ethjs-ens',
+ () =>
+ class MocKENS {
+ async ensLookup() {
+ return '';
+ }
+ },
+);
+
+const baseStore = {
+ send: initialState,
+ ENS: ensInitialState,
+ gas: {
+ basicEstimateStatus: 'READY',
+ basicEstimates: { slow: '0x0', average: '0x1', fast: '0x2' },
+ customData: { limit: null, price: null },
+ },
+ history: { mostRecentOverviewPage: 'activity' },
+ metamask: {
+ tokens: [],
+ preferences: {
+ useNativeCurrencyAsPrimaryCurrency: false,
+ },
+ currentCurrency: 'USD',
+ provider: {
+ chainId: RINKEBY_CHAIN_ID,
+ },
+ nativeCurrency: 'ETH',
+ featureFlags: {
+ sendHexData: false,
+ },
+ addressBook: {
+ [RINKEBY_CHAIN_ID]: [],
+ },
+ cachedBalances: {
+ [RINKEBY_CHAIN_ID]: {},
+ },
+ accounts: {
+ '0x0': { balance: '0x0', address: '0x0' },
+ },
+ identities: { '0x0': {} },
+ },
+};
+
+describe('Send Page', () => {
+ describe('Send Flow Initialization', () => {
+ it('should initialize the send, ENS, and gas slices on render', () => {
+ const store = configureMockStore(middleware)(baseStore);
+ renderWithProvider(, store);
+ const actions = store.getActions();
+ expect(actions).toStrictEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ type: 'ENS/enableEnsLookup',
+ }),
+ expect.objectContaining({
+ type: 'send/initializeSendState/pending',
+ }),
+ expect.objectContaining({
+ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
+ }),
+ expect.objectContaining({
+ type: 'metamask/gas/SET_ESTIMATE_SOURCE',
+ }),
+ ]),
+ );
+ });
+
+ it('should showQrScanner when location.search is ?scan=true', () => {
+ useLocation.mockImplementation(() => ({ search: '?scan=true' }));
+ const store = configureMockStore(middleware)(baseStore);
+ renderWithProvider(, store);
+ const actions = store.getActions();
+ expect(actions).toStrictEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ type: 'ENS/enableEnsLookup',
+ }),
+ expect.objectContaining({
+ type: 'send/initializeSendState/pending',
+ }),
+ expect.objectContaining({
+ type: 'metamask/gas/BASIC_GAS_ESTIMATE_STATUS',
+ }),
+ expect.objectContaining({
+ type: 'metamask/gas/SET_ESTIMATE_SOURCE',
+ }),
+ expect.objectContaining({
+ type: 'UI_MODAL_OPEN',
+ payload: { name: 'QR_SCANNER' },
+ }),
+ ]),
+ );
+ useLocation.mockImplementation(() => ({ search: '' }));
+ });
+ });
+
+ describe('Add Recipient Flow', () => {
+ it('should render the header with Add Recipient displayed', () => {
+ const store = configureMockStore(middleware)(baseStore);
+ const { getByText } = renderWithProvider(, store);
+ expect(getByText('Add Recipient')).toBeTruthy();
+ });
+
+ it('should render the EnsInput field', () => {
+ const store = configureMockStore(middleware)(baseStore);
+ const { getByPlaceholderText } = renderWithProvider(, store);
+ expect(
+ getByPlaceholderText('Search, public address (0x), or ENS'),
+ ).toBeTruthy();
+ });
+
+ it('should not render the footer', () => {
+ const store = configureMockStore(middleware)(baseStore);
+ const { queryByText } = renderWithProvider(, store);
+ expect(queryByText('Next')).toBeNull();
+ });
+ });
+
+ describe('Send and Edit Flow', () => {
+ it('should render the header with Send displayed', () => {
+ const store = configureMockStore(middleware)({
+ ...baseStore,
+ send: { ...baseStore.send, stage: SEND_STAGES.DRAFT },
+ });
+ const { getByText } = renderWithProvider(, store);
+ expect(getByText('Send')).toBeTruthy();
+ });
+
+ it('should render the EnsInput field', () => {
+ const store = configureMockStore(middleware)(baseStore);
+ const { getByPlaceholderText } = renderWithProvider(, store);
+ expect(
+ getByPlaceholderText('Search, public address (0x), or ENS'),
+ ).toBeTruthy();
+ });
+
+ it('should render the footer', () => {
+ const store = configureMockStore(middleware)({
+ ...baseStore,
+ send: { ...baseStore.send, stage: SEND_STAGES.DRAFT },
+ });
+ const { getByText } = renderWithProvider(, store);
+ expect(getByText('Next')).toBeTruthy();
+ });
+ });
+});
diff --git a/ui/pages/send/send.utils.js b/ui/pages/send/send.utils.js
index 1d7fb3562..d4d4e7670 100644
--- a/ui/pages/send/send.utils.js
+++ b/ui/pages/send/send.utils.js
@@ -11,28 +11,14 @@ import {
import { calcTokenAmount } from '../../helpers/utils/token-util';
import { addHexPrefix } from '../../../app/scripts/lib/util';
-import { GAS_LIMITS } from '../../../shared/constants/gas';
-import {
- INSUFFICIENT_FUNDS_ERROR,
- INSUFFICIENT_TOKENS_ERROR,
- MIN_GAS_LIMIT_HEX,
- NEGATIVE_ETH_ERROR,
- TOKEN_TRANSFER_FUNCTION_SIGNATURE,
-} from './send.constants';
+import { TOKEN_TRANSFER_FUNCTION_SIGNATURE } from './send.constants';
export {
addGasBuffer,
calcGasTotal,
- calcTokenBalance,
- doesAmountErrorRequireUpdate,
- estimateGasForSend,
generateTokenTransferData,
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
isBalanceSufficient,
isTokenBalanceSufficient,
- removeLeadingZeroes,
ellipsify,
};
@@ -93,186 +79,6 @@ function isTokenBalanceSufficient({ amount = '0x0', tokenBalance, decimals }) {
return tokenBalanceIsSufficient;
}
-function getAmountErrorObject({
- amount,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- sendToken,
- tokenBalance,
-}) {
- let insufficientFunds = false;
- if (gasTotal && conversionRate && !sendToken) {
- insufficientFunds = !isBalanceSufficient({
- amount,
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- });
- }
-
- let inSufficientTokens = false;
- if (sendToken && tokenBalance !== null) {
- const { decimals } = sendToken;
- inSufficientTokens = !isTokenBalanceSufficient({
- tokenBalance,
- amount,
- decimals,
- });
- }
-
- const amountLessThanZero = conversionGreaterThan(
- { value: 0, fromNumericBase: 'dec' },
- { value: amount, fromNumericBase: 'hex' },
- );
-
- let amountError = null;
-
- if (insufficientFunds) {
- amountError = INSUFFICIENT_FUNDS_ERROR;
- } else if (inSufficientTokens) {
- amountError = INSUFFICIENT_TOKENS_ERROR;
- } else if (amountLessThanZero) {
- amountError = NEGATIVE_ETH_ERROR;
- }
-
- return { amount: amountError };
-}
-
-function getGasFeeErrorObject({
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
-}) {
- let gasFeeError = null;
-
- if (gasTotal && conversionRate) {
- const insufficientFunds = !isBalanceSufficient({
- amount: '0x0',
- balance,
- conversionRate,
- gasTotal,
- primaryCurrency,
- });
-
- if (insufficientFunds) {
- gasFeeError = INSUFFICIENT_FUNDS_ERROR;
- }
- }
-
- return { gasFee: gasFeeError };
-}
-
-function calcTokenBalance({ sendToken, usersToken }) {
- const { decimals } = sendToken || {};
- return calcTokenAmount(usersToken.balance.toString(), decimals).toString(16);
-}
-
-function doesAmountErrorRequireUpdate({
- balance,
- gasTotal,
- prevBalance,
- prevGasTotal,
- prevTokenBalance,
- sendToken,
- tokenBalance,
-}) {
- const balanceHasChanged = balance !== prevBalance;
- const gasTotalHasChange = gasTotal !== prevGasTotal;
- const tokenBalanceHasChanged = sendToken && tokenBalance !== prevTokenBalance;
- const amountErrorRequiresUpdate =
- balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged;
-
- return amountErrorRequiresUpdate;
-}
-
-async function estimateGasForSend({
- selectedAddress,
- sendToken,
- blockGasLimit = MIN_GAS_LIMIT_HEX,
- to,
- value,
- data,
- gasPrice,
- estimateGasMethod,
-}) {
- const paramsForGasEstimate = { from: selectedAddress, value, gasPrice };
-
- // if recipient has no code, gas is 21k max:
- if (!sendToken && !data) {
- const code = Boolean(to) && (await global.eth.getCode(to));
- // Geth will return '0x', and ganache-core v2.2.1 will return '0x0'
- const codeIsEmpty = !code || code === '0x' || code === '0x0';
- if (codeIsEmpty) {
- return GAS_LIMITS.SIMPLE;
- }
- } else if (sendToken && !to) {
- return GAS_LIMITS.BASE_TOKEN_ESTIMATE;
- }
-
- if (sendToken) {
- paramsForGasEstimate.value = '0x0';
- paramsForGasEstimate.data = generateTokenTransferData({
- toAddress: to,
- amount: value,
- sendToken,
- });
- paramsForGasEstimate.to = sendToken.address;
- } else {
- if (data) {
- paramsForGasEstimate.data = data;
- }
-
- if (to) {
- paramsForGasEstimate.to = to;
- }
-
- if (!value || value === '0') {
- paramsForGasEstimate.value = '0xff';
- }
- }
-
- // if not, fall back to block gasLimit
- if (!blockGasLimit) {
- // eslint-disable-next-line no-param-reassign
- blockGasLimit = MIN_GAS_LIMIT_HEX;
- }
-
- paramsForGasEstimate.gas = addHexPrefix(
- multiplyCurrencies(blockGasLimit, 0.95, {
- multiplicandBase: 16,
- multiplierBase: 10,
- roundDown: '0',
- toNumericBase: 'hex',
- }),
- );
-
- // run tx
- try {
- const estimatedGas = await estimateGasMethod(paramsForGasEstimate);
- const estimateWithBuffer = addGasBuffer(estimatedGas, blockGasLimit, 1.5);
- return addHexPrefix(estimateWithBuffer);
- } catch (error) {
- const simulationFailed =
- error.message.includes('Transaction execution error.') ||
- error.message.includes(
- 'gas required exceeds allowance or always failing transaction',
- );
- if (simulationFailed) {
- const estimateWithBuffer = addGasBuffer(
- paramsForGasEstimate.gas,
- blockGasLimit,
- 1.5,
- );
- return addHexPrefix(estimateWithBuffer);
- }
- throw error;
- }
-}
-
function addGasBuffer(
initialGasLimitHex,
blockGasLimitHex,
@@ -339,16 +145,6 @@ function generateTokenTransferData({
);
}
-function getToAddressForGasUpdate(...addresses) {
- return [...addresses, '']
- .find((str) => str !== undefined && str !== null)
- .toLowerCase();
-}
-
-function removeLeadingZeroes(str) {
- return str.replace(/^0*(?=\d)/u, '');
-}
-
function ellipsify(text, first = 6, last = 4) {
return `${text.slice(0, first)}...${text.slice(-last)}`;
}
diff --git a/ui/pages/send/send.utils.test.js b/ui/pages/send/send.utils.test.js
index 02b45f1fa..7960b4aca 100644
--- a/ui/pages/send/send.utils.test.js
+++ b/ui/pages/send/send.utils.test.js
@@ -1,4 +1,3 @@
-import sinon from 'sinon';
import { rawEncode } from 'ethereumjs-abi';
import {
@@ -8,26 +7,13 @@ import {
conversionUtil,
} from '../../helpers/utils/conversion-util';
-import { GAS_LIMITS } from '../../../shared/constants/gas';
import {
calcGasTotal,
- estimateGasForSend,
- doesAmountErrorRequireUpdate,
generateTokenTransferData,
- getAmountErrorObject,
- getGasFeeErrorObject,
- getToAddressForGasUpdate,
- calcTokenBalance,
isBalanceSufficient,
isTokenBalanceSufficient,
- removeLeadingZeroes,
} from './send.utils';
-import {
- INSUFFICIENT_FUNDS_ERROR,
- INSUFFICIENT_TOKENS_ERROR,
-} from './send.constants';
-
jest.mock('../../helpers/utils/conversion-util', () => ({
addCurrencies: jest.fn((a, b) => {
let [a1, b1] = [a, b];
@@ -67,44 +53,6 @@ describe('send utils', () => {
});
});
- describe('doesAmountErrorRequireUpdate()', () => {
- const config = {
- 'should return true if balances are different': {
- balance: 0,
- prevBalance: 1,
- expectedResult: true,
- },
- 'should return true if gasTotals are different': {
- gasTotal: 0,
- prevGasTotal: 1,
- expectedResult: true,
- },
- 'should return true if token balances are different': {
- tokenBalance: 0,
- prevTokenBalance: 1,
- sendToken: { address: '0x0' },
- expectedResult: true,
- },
- 'should return false if they are all the same': {
- balance: 1,
- prevBalance: 1,
- gasTotal: 1,
- prevGasTotal: 1,
- tokenBalance: 1,
- prevTokenBalance: 1,
- sendToken: { address: '0x0' },
- expectedResult: false,
- },
- };
- Object.entries(config).forEach(([description, obj]) => {
- it(`${description}`, () => {
- expect(doesAmountErrorRequireUpdate(obj)).toStrictEqual(
- obj.expectedResult,
- );
- });
- });
- });
-
describe('generateTokenTransferData()', () => {
it('should return undefined if not passed a send token', () => {
expect(
@@ -141,86 +89,6 @@ describe('send utils', () => {
});
});
- describe('getAmountErrorObject()', () => {
- const config = {
- 'should return insufficientFunds error if isBalanceSufficient returns false': {
- amount: 15,
- balance: 1,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
- },
- 'should not return insufficientFunds error if sendToken is truthy': {
- amount: '0x0',
- balance: 1,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- sendToken: { address: '0x0', symbol: 'DEF', decimals: 0 },
- decimals: 0,
- tokenBalance: 'sometokenbalance',
- expectedResult: { amount: null },
- },
- 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
- amount: '0x10',
- balance: 100,
- conversionRate: 3,
- decimals: 10,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- sendToken: { address: '0x0' },
- tokenBalance: 123,
- expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR },
- },
- };
- Object.entries(config).forEach(([description, obj]) => {
- it(`${description}`, () => {
- expect(getAmountErrorObject(obj)).toStrictEqual(obj.expectedResult);
- });
- });
- });
-
- describe('getGasFeeErrorObject()', () => {
- const config = {
- 'should return insufficientFunds error if isBalanceSufficient returns false': {
- balance: 16,
- conversionRate: 3,
- gasTotal: 17,
- primaryCurrency: 'ABC',
- expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR },
- },
- 'should return null error if isBalanceSufficient returns true': {
- balance: 16,
- conversionRate: 3,
- gasTotal: 15,
- primaryCurrency: 'ABC',
- expectedResult: { gasFee: null },
- },
- };
- Object.entries(config).forEach(([description, obj]) => {
- it(`${description}`, () => {
- expect(getGasFeeErrorObject(obj)).toStrictEqual(obj.expectedResult);
- });
- });
- });
-
- describe('calcTokenBalance()', () => {
- it('should return the calculated token balance', () => {
- expect(
- calcTokenBalance({
- sendToken: {
- address: '0x0',
- decimals: 11,
- },
- usersToken: {
- balance: 20,
- },
- }),
- ).toStrictEqual('calc:2011');
- });
- });
-
describe('isBalanceSufficient()', () => {
it('should correctly call addCurrencies and return the result of calling conversionGTE', () => {
const result = isBalanceSufficient({
@@ -279,201 +147,4 @@ describe('send utils', () => {
expect(result).toStrictEqual(false);
});
});
-
- describe('estimateGasForSend', () => {
- const baseMockParams = {
- blockGasLimit: '0x64',
- selectedAddress: 'mockAddress',
- to: '0xisContract',
- estimateGasMethod: sinon.stub().callsFake(({ to }) => {
- if (typeof to === 'string' && to.match(/willFailBecauseOf:/u)) {
- throw new Error(to.match(/:(.+)$/u)[1]);
- }
- return '0xabc16';
- }),
- };
- const baseexpectedCall = {
- from: 'mockAddress',
- gas: '0x64x0.95',
- to: '0xisContract',
- value: '0xff',
- };
-
- beforeEach(() => {
- global.eth = {
- getCode: sinon
- .stub()
- .callsFake((address) =>
- Promise.resolve(address.match(/isContract/u) ? 'not-0x' : '0x'),
- ),
- };
- });
-
- afterEach(() => {
- baseMockParams.estimateGasMethod.resetHistory();
- global.eth.getCode.resetHistory();
- });
-
- it('should call ethQuery.estimateGasForSend with the expected params', async () => {
- const result = await estimateGasForSend(baseMockParams);
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(1);
- expect(baseMockParams.estimateGasMethod.getCall(0).args[0]).toStrictEqual(
- {
- gasPrice: undefined,
- value: undefined,
- ...baseexpectedCall,
- },
- );
- expect(result).toStrictEqual('0xabc16');
- });
-
- it('should call ethQuery.estimateGasForSend with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => {
- const result = await estimateGasForSend({
- ...baseMockParams,
- blockGasLimit: '0xbcd',
- });
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(1);
- expect(baseMockParams.estimateGasMethod.getCall(0).args[0]).toStrictEqual(
- {
- gasPrice: undefined,
- value: undefined,
- ...baseexpectedCall,
- gas: '0xbcdx0.95',
- },
- );
- expect(result).toStrictEqual('0xabc16x1.5');
- });
-
- it('should call ethQuery.estimateGasForSend with a value of 0x0 and the expected data and to if passed a sendToken', async () => {
- const result = await estimateGasForSend({
- data: 'mockData',
- sendToken: { address: 'mockAddress' },
- ...baseMockParams,
- });
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(1);
- expect(baseMockParams.estimateGasMethod.getCall(0).args[0]).toStrictEqual(
- {
- ...baseexpectedCall,
- gasPrice: undefined,
- value: '0x0',
- data: '0xa9059cbb',
- to: 'mockAddress',
- },
- );
- expect(result).toStrictEqual('0xabc16');
- });
-
- it('should call ethQuery.estimateGasForSend without a recipient if the recipient is empty and data passed', async () => {
- const data = 'mockData';
- const to = '';
- const result = await estimateGasForSend({ ...baseMockParams, data, to });
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(1);
- expect(baseMockParams.estimateGasMethod.getCall(0).args[0]).toStrictEqual(
- {
- gasPrice: undefined,
- value: '0xff',
- data,
- from: baseexpectedCall.from,
- gas: baseexpectedCall.gas,
- },
- );
- expect(result).toStrictEqual('0xabc16');
- });
-
- it(`should return ${GAS_LIMITS.SIMPLE} if ethQuery.getCode does not return '0x'`, async () => {
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0);
- const result = await estimateGasForSend({
- ...baseMockParams,
- to: '0x123',
- });
- expect(result).toStrictEqual(GAS_LIMITS.SIMPLE);
- });
-
- it(`should return ${GAS_LIMITS.SIMPLE} if not passed a sendToken or truthy to address`, async () => {
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0);
- const result = await estimateGasForSend({ ...baseMockParams, to: null });
- expect(result).toStrictEqual(GAS_LIMITS.SIMPLE);
- });
-
- it(`should not return ${GAS_LIMITS.SIMPLE} if passed a sendToken`, async () => {
- expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0);
- const result = await estimateGasForSend({
- ...baseMockParams,
- to: '0x123',
- sendToken: { address: '0x0' },
- });
- expect(result).not.toStrictEqual(GAS_LIMITS.SIMPLE);
- });
-
- it(`should return ${GAS_LIMITS.BASE_TOKEN_ESTIMATE} if passed a sendToken but no to address`, async () => {
- const result = await estimateGasForSend({
- ...baseMockParams,
- to: null,
- sendToken: { address: '0x0' },
- });
- expect(result).toStrictEqual(GAS_LIMITS.BASE_TOKEN_ESTIMATE);
- });
-
- it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
- const result = await estimateGasForSend({
- ...baseMockParams,
- to: 'isContract willFailBecauseOf:Transaction execution error.',
- });
- expect(result).toStrictEqual('0x64x0.95');
- });
-
- it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => {
- const result = await estimateGasForSend({
- ...baseMockParams,
- to:
- 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.',
- });
- expect(result).toStrictEqual('0x64x0.95');
- });
-
- it(`should reject other errors`, async () => {
- await expect(
- estimateGasForSend({
- ...baseMockParams,
- to: 'isContract willFailBecauseOf:some other error',
- }),
- ).rejects.toThrow('some other error');
- });
- });
-
- describe('getToAddressForGasUpdate()', () => {
- it('should return empty string if all params are undefined or null', () => {
- expect(getToAddressForGasUpdate(undefined, null)).toStrictEqual('');
- });
-
- it('should return the first string that is not defined or null in lower case', () => {
- expect(getToAddressForGasUpdate('A', null)).toStrictEqual('a');
- expect(getToAddressForGasUpdate(undefined, 'B')).toStrictEqual('b');
- });
- });
-
- describe('removeLeadingZeroes()', () => {
- it('should remove leading zeroes from int when user types', () => {
- expect(removeLeadingZeroes('0')).toStrictEqual('0');
- expect(removeLeadingZeroes('1')).toStrictEqual('1');
- expect(removeLeadingZeroes('00')).toStrictEqual('0');
- expect(removeLeadingZeroes('01')).toStrictEqual('1');
- });
-
- it('should remove leading zeroes from int when user copy/paste', () => {
- expect(removeLeadingZeroes('001')).toStrictEqual('1');
- });
-
- it('should remove leading zeroes from float when user types', () => {
- expect(removeLeadingZeroes('0.')).toStrictEqual('0.');
- expect(removeLeadingZeroes('0.0')).toStrictEqual('0.0');
- expect(removeLeadingZeroes('0.00')).toStrictEqual('0.00');
- expect(removeLeadingZeroes('0.001')).toStrictEqual('0.001');
- expect(removeLeadingZeroes('0.10')).toStrictEqual('0.10');
- });
-
- it('should remove leading zeroes from float when user copy/paste', () => {
- expect(removeLeadingZeroes('00.1')).toStrictEqual('0.1');
- });
- });
});
diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js
index e454838d1..64e076973 100644
--- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js
+++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.component.js
@@ -11,6 +11,7 @@ import {
isBurnAddress,
isValidHexAddress,
} from '../../../../../shared/modules/hexstring-utils';
+import { INVALID_RECIPIENT_ADDRESS_ERROR } from '../../../send/send.constants';
export default class AddContact extends PureComponent {
static contextTypes = {
@@ -24,28 +25,32 @@ export default class AddContact extends PureComponent {
qrCodeData:
PropTypes.object /* eslint-disable-line react/no-unused-prop-types */,
qrCodeDetected: PropTypes.func,
+ ensResolution: PropTypes.string,
+ ensError: PropTypes.string,
+ resetResolution: PropTypes.func,
};
state = {
newName: '',
ethAddress: '',
- ensAddress: '',
error: '',
- ensError: '',
+ input: '',
};
constructor(props) {
super(props);
- this.dValidate = debounce(this.validate, 1000);
+ this.dValidate = debounce(this.validate, 500);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.qrCodeData) {
if (nextProps.qrCodeData.type === 'address') {
+ const { ensResolution } = this.props;
const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase();
- const currentAddress = this.state.ensAddress || this.state.ethAddress;
+ const currentAddress = ensResolution || this.state.ethAddress;
if (currentAddress.toLowerCase() !== scannedAddress) {
- this.setState({ ethAddress: scannedAddress, ensAddress: '' });
+ this.setState({ input: scannedAddress });
+ this.validate(scannedAddress);
// Clean up QR code data after handling
this.props.qrCodeDetected(null);
}
@@ -62,43 +67,48 @@ export default class AddContact extends PureComponent {
if (valid || validEnsAddress || address === '') {
this.setState({ error: '', ethAddress: address });
} else {
- this.setState({ error: 'Invalid Address' });
+ this.setState({ error: INVALID_RECIPIENT_ADDRESS_ERROR });
}
};
+ onChange = (input) => {
+ this.setState({ input });
+ this.dValidate(input);
+ };
+
renderInput() {
return (
{
this.props.scanQrCode();
}}
- onChange={this.dValidate}
- onPaste={(text) => this.setState({ ethAddress: text })}
- onReset={() => this.setState({ ethAddress: '', ensAddress: '' })}
- updateEnsResolution={(address) => {
- this.setState({ ensAddress: address, error: '', ensError: '' });
+ onChange={this.onChange}
+ onPaste={(text) => {
+ this.setState({ input: text });
+ this.validate(text);
}}
- updateEnsResolutionError={(message) =>
- this.setState({ ensError: message })
- }
- value={this.state.ethAddress || ''}
+ onReset={() => {
+ this.props.resetResolution();
+ this.setState({ ethAddress: '', input: '' });
+ }}
+ userInput={this.state.input}
/>
);
}
render() {
const { t } = this.context;
- const { history, addToAddressBook } = this.props;
+ const { history, addToAddressBook, ensError, ensResolution } = this.props;
- const errorToRender = this.state.ensError || this.state.error;
+ const errorToRender = ensError || this.state.error;
return (
- {this.state.ensAddress && (
+ {ensResolution && (
-
+
- {this.state.ensAddress}
+ {ensResolution}
)}
@@ -124,7 +134,7 @@ export default class AddContact extends PureComponent {
{this.renderInput()}
{errorToRender && (
- {errorToRender}
+ {t(errorToRender)}
)}
@@ -134,7 +144,7 @@ export default class AddContact extends PureComponent {
disabled={Boolean(this.state.error)}
onSubmit={async () => {
await addToAddressBook(
- this.state.ensAddress || this.state.ethAddress,
+ ensResolution || this.state.ethAddress,
this.state.newName,
);
history.push(CONTACT_LIST_ROUTE);
diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js
index 8d3c63c5f..49f4deb70 100644
--- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js
+++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.container.js
@@ -6,12 +6,19 @@ import {
showQrScanner,
qrCodeDetected,
} from '../../../../store/actions';
-import { getQrCodeData } from '../../../../selectors';
+import { getQrCodeData } from '../../../../ducks/app/app';
+import {
+ getEnsError,
+ getEnsResolution,
+ resetResolution,
+} from '../../../../ducks/ens';
import AddContact from './add-contact.component';
const mapStateToProps = (state) => {
return {
qrCodeData: getQrCodeData(state),
+ ensError: getEnsError(state),
+ ensResolution: getEnsResolution(state),
};
};
@@ -21,6 +28,7 @@ const mapDispatchToProps = (dispatch) => {
dispatch(addToAddressBook(recipient, nickname)),
scanQrCode: () => dispatch(showQrScanner()),
qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
+ resetResolution: () => dispatch(resetResolution()),
};
};
diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
index 1c8e132a0..a0651b817 100644
--- a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
+++ b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js
@@ -49,8 +49,8 @@ export default class ContactListTab extends Component {
return (
-
-
{t('builContactList')}
+
+
{t('buildContactList')}
{t('addFriendsAndAddresses')}
diff --git a/ui/pages/settings/info-tab/info-tab.component.js b/ui/pages/settings/info-tab/info-tab.component.js
index be821a154..d601cbe74 100644
--- a/ui/pages/settings/info-tab/info-tab.component.js
+++ b/ui/pages/settings/info-tab/info-tab.component.js
@@ -104,7 +104,7 @@ export default class InfoTab extends PureComponent {
-
{t('builtInCalifornia')}
+
{t('builtAroundTheWorld')}
{this.renderInfoLinks()}
diff --git a/ui/selectors/confirm-transaction.js b/ui/selectors/confirm-transaction.js
index 70982d1b1..c721e12b1 100644
--- a/ui/selectors/confirm-transaction.js
+++ b/ui/selectors/confirm-transaction.js
@@ -12,6 +12,7 @@ import {
import { sumHexes } from '../helpers/utils/transactions.util';
import { transactionMatchesNetwork } from '../../shared/modules/transaction.utils';
import { getNativeCurrency } from '../ducks/metamask/metamask';
+import { getAveragePriceEstimateInHexWEI } from './custom-gas';
import { getCurrentChainId, deprecatedGetCurrentNetworkId } from './selectors';
const unapprovedTxsSelector = (state) => state.metamask.unapprovedTxs;
@@ -158,7 +159,7 @@ const contractExchangeRatesSelector = (state) =>
const tokenDecimalsSelector = createSelector(
tokenPropsSelector,
- (tokenProps) => tokenProps && tokenProps.tokenDecimals,
+ (tokenProps) => tokenProps && tokenProps.decimals,
);
const tokenDataArgsSelector = createSelector(
@@ -218,10 +219,18 @@ export const transactionFeeSelector = function (state, txData) {
const conversionRate = conversionRateSelector(state);
const nativeCurrency = getNativeCurrency(state);
- const {
- txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {},
+ const { txParams: { value = '0x0', gas: gasLimit = '0x0' } = {} } = txData;
+
+ // if the gas price from our infura endpoint is null or undefined
+ // use the metaswap average price estimation as a fallback
+ let {
+ txParams: { gasPrice },
} = txData;
+ if (!gasPrice) {
+ gasPrice = getAveragePriceEstimateInHexWEI(state) || '0x0';
+ }
+
const fiatTransactionAmount = getValueFromWeiHex({
value,
fromCurrency: nativeCurrency,
diff --git a/ui/selectors/confirm-transaction.test.js b/ui/selectors/confirm-transaction.test.js
index 9a899a523..8ea9b28dd 100644
--- a/ui/selectors/confirm-transaction.test.js
+++ b/ui/selectors/confirm-transaction.test.js
@@ -58,8 +58,8 @@ describe('Confirm Transaction Selector', () => {
}),
},
tokenProps: {
- tokenDecimals: '2',
- tokenSymbol: 'META',
+ decimals: '2',
+ symbol: 'META',
},
},
};
diff --git a/ui/selectors/custom-gas.js b/ui/selectors/custom-gas.js
index 4f905a7fe..a357bf758 100644
--- a/ui/selectors/custom-gas.js
+++ b/ui/selectors/custom-gas.js
@@ -9,14 +9,10 @@ import { formatETHFee } from '../helpers/utils/formatters';
import { calcGasTotal } from '../pages/send/send.utils';
import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common';
+import { getGasPrice } from '../ducks/send';
import { BASIC_ESTIMATE_STATES, GAS_SOURCE } from '../ducks/gas/gas.duck';
import { GAS_LIMITS } from '../../shared/constants/gas';
-import {
- getCurrentCurrency,
- getIsMainnet,
- getPreferences,
- getGasPrice,
-} from '.';
+import { getCurrentCurrency, getIsMainnet, getShouldShowFiat } from '.';
const NUMBER_OF_DECIMALS_SM_BTNS = 5;
@@ -288,9 +284,7 @@ export function getRenderableBasicEstimateData(state, gasLimit) {
return [];
}
- const { showFiatInTestnets } = getPreferences(state);
- const isMainnet = getIsMainnet(state);
- const showFiat = isMainnet || Boolean(showFiatInTestnets);
+ const showFiat = getShouldShowFiat(state);
const { conversionRate } = state.metamask;
const currentCurrency = getCurrentCurrency(state);
@@ -313,12 +307,9 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
if (getBasicGasEstimateLoadingStatus(state)) {
return [];
}
-
- const { showFiatInTestnets } = getPreferences(state);
- const isMainnet = getIsMainnet(state);
- const showFiat = isMainnet || Boolean(showFiatInTestnets);
+ const showFiat = getShouldShowFiat(state);
const gasLimit =
- state.send.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
+ state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
const { conversionRate } = state.metamask;
const currentCurrency = getCurrentCurrency(state);
const {
diff --git a/ui/selectors/custom-gas.test.js b/ui/selectors/custom-gas.test.js
index 91344b96b..fb383248d 100644
--- a/ui/selectors/custom-gas.test.js
+++ b/ui/selectors/custom-gas.test.js
@@ -112,7 +112,9 @@ describe('custom-gas selectors', () => {
it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', () => {
const mockState = {
send: {
- gasPrice: '0x28bed0160',
+ gas: {
+ gasPrice: '0x28bed0160',
+ },
},
gas: {
customData: { price: null },
@@ -124,7 +126,9 @@ describe('custom-gas selectors', () => {
it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', () => {
const mockState = {
send: {
- gasPrice: '0x30e4f9b400',
+ gas: {
+ gasPrice: '0x30e4f9b400',
+ },
},
gas: {
customData: { price: null },
@@ -226,7 +230,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -277,7 +283,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -328,7 +336,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -373,7 +383,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -434,7 +446,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -479,7 +493,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -530,7 +546,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -581,7 +599,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
@@ -626,7 +646,9 @@ describe('custom-gas selectors', () => {
},
},
send: {
- gasLimit: GAS_LIMITS.SIMPLE,
+ gas: {
+ gasLimit: GAS_LIMITS.SIMPLE,
+ },
},
gas: {
basicEstimates: {
diff --git a/ui/selectors/index.js b/ui/selectors/index.js
index b82c59c05..3f4ff3b0e 100644
--- a/ui/selectors/index.js
+++ b/ui/selectors/index.js
@@ -3,5 +3,4 @@ export * from './custom-gas';
export * from './first-time-flow';
export * from './permissions';
export * from './selectors';
-export * from './send';
export * from './transactions';
diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js
index a41005a7e..615d7df51 100644
--- a/ui/selectors/selectors.js
+++ b/ui/selectors/selectors.js
@@ -24,7 +24,10 @@ import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/temp
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { DAY } from '../../shared/constants/time';
-import { getNativeCurrency } from '../ducks/metamask/metamask';
+import {
+ getNativeCurrency,
+ getConversionRate,
+} from '../ducks/metamask/metamask';
/**
* One of the only remaining valid uses of selecting the network subkey of the
@@ -379,8 +382,9 @@ export function getPreferences({ metamask }) {
export function getShouldShowFiat(state) {
const isMainNet = getIsMainnet(state);
+ const conversionRate = getConversionRate(state);
const { showFiatInTestnets } = getPreferences(state);
- return Boolean(isMainNet || showFiatInTestnets);
+ return Boolean((isMainNet || showFiatInTestnets) && conversionRate);
}
export function getShouldHideZeroBalanceTokens(state) {
diff --git a/ui/selectors/send-selectors-test-data.js b/ui/selectors/send-selectors-test-data.js
deleted file mode 100644
index b2663aadb..000000000
--- a/ui/selectors/send-selectors-test-data.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
-
-const state = {
- metamask: {
- isInitialized: true,
- isUnlocked: true,
- featureFlags: { sendHexData: true },
- identities: {
- '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
- address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- name: 'Send Account 1',
- },
- '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- name: 'Send Account 2',
- },
- '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
- address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- name: 'Send Account 3',
- },
- '0xd85a4b6a394794842887b8284293d69163007bbb': {
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- },
- },
- cachedBalances: {},
- currentBlockGasLimit: '0x4c1878',
- currentCurrency: 'USD',
- conversionRate: 1200.88200327,
- conversionDate: 1489013762,
- nativeCurrency: 'ETH',
- frequentRpcList: [],
- network: '3',
- provider: {
- type: 'testnet',
- chainId: '0x3',
- },
- accounts: {
- '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': {
- code: '0x',
- balance: '0x47c9d71831c76efe',
- nonce: '0x1b',
- address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- },
- '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': {
- code: '0x',
- balance: '0x37452b1315889f80',
- nonce: '0xa',
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- },
- '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': {
- code: '0x',
- balance: '0x30c9d71831c76efe',
- nonce: '0x1c',
- address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- },
- '0xd85a4b6a394794842887b8284293d69163007bbb': {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- },
- addressBook: {
- '0x3': {
- '0x06195827297c7a80a443b6894d3bdb8824b43896': {
- address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
- name: 'Address Book Account 1',
- chainId: '0x3',
- },
- },
- },
- tokens: [
- {
- address: '0x1a195821297c7a80a433b6894d3bdb8824b43896',
- decimals: 18,
- symbol: 'ABC',
- },
- {
- address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'DEF',
- },
- {
- address: '0xa42084c8d1d9a2198631988579bb36b48433a72b',
- decimals: 18,
- symbol: 'GHI',
- },
- ],
- transactions: {},
- currentNetworkTxList: [
- {
- id: 'mockTokenTx1',
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- from: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- time: 1700000000000,
- },
- {
- id: 'mockTokenTx2',
- txParams: {
- to: '0xafaketokenaddress',
- from: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- time: 1600000000000,
- },
- {
- id: 'mockTokenTx3',
- txParams: {
- to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- from: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- time: 1500000000000,
- },
- {
- id: 'mockEthTx1',
- txParams: {
- to: '0xd85a4b6a394794842887b8284293d69163007bbb',
- from: '0xd85a4b6a394794842887b8284293d69163007bbb',
- },
- time: 1400000000000,
- },
- ],
- unapprovedMsgs: {
- '0xabc': { id: 'unapprovedMessage1', time: 1650000000000 },
- '0xdef': { id: 'unapprovedMessage2', time: 1550000000000 },
- '0xghi': { id: 'unapprovedMessage3', time: 1450000000000 },
- },
- unapprovedMsgCount: 0,
- unapprovedPersonalMsgs: {},
- unapprovedPersonalMsgCount: 0,
- unapprovedDecryptMsgs: {},
- unapprovedDecryptMsgCount: 0,
- unapprovedEncryptionPublicKeyMsgs: {},
- unapprovedEncryptionPublicKeyMsgCount: 0,
- keyringTypes: ['Simple Key Pair', 'HD Key Tree'],
- keyrings: [
- {
- type: 'HD Key Tree',
- accounts: [
- 'fdea65c8e26263f6d9a1b5de9555d2931a33b825',
- 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- '2f8d4a878cfa04a6e60d46362f5644deab66572d',
- ],
- },
- {
- type: 'Simple Key Pair',
- accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'],
- },
- ],
- selectedAddress: '0xd85a4b6a394794842887b8284293d69163007bbb',
- unapprovedTxs: {
- 4768706228115573: {
- id: 4768706228115573,
- time: 1487363153561,
- status: TRANSACTION_STATUSES.UNAPPROVED,
- gasMultiplier: 1,
- metamaskNetworkId: '3',
- txParams: {
- from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
- value: '0xde0b6b3a7640000',
- metamaskId: 4768706228115573,
- metamaskNetworkId: '3',
- gas: '0x5209',
- },
- txFee: '17e0186e60800',
- txValue: 'de0b6b3a7640000',
- maxCost: 'de234b52e4a0800',
- gasPrice: '4a817c800',
- },
- },
- currentLocale: 'en',
- },
- appState: {
- menuOpen: false,
- currentView: {
- name: 'accountDetail',
- detailView: null,
- context: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
- },
- accountDetail: {
- subview: 'transactions',
- },
- modal: {
- modalState: {},
- previousModalState: {},
- },
- isLoading: false,
- warning: null,
- scrollToBottom: false,
- forgottenPassword: null,
- },
- identities: {},
- send: {
- fromDropdownOpen: false,
- gasLimit: '0xFFFF',
- gasPrice: '0xaa',
- gasTotal: '0xb451dc41b578',
- tokenBalance: 3434,
- from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- to: '0x987fedabc',
- amount: '0x080',
- memo: '',
- errors: {
- someError: null,
- },
- maxModeOn: false,
- editingTransactionId: 97531,
- },
-};
-
-export default state;
diff --git a/ui/selectors/send.js b/ui/selectors/send.js
deleted file mode 100644
index 3d0b11d83..000000000
--- a/ui/selectors/send.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import abi from 'human-standard-token-abi';
-import { calcGasTotal } from '../pages/send/send.utils';
-import {
- getSelectedAccount,
- getTargetAccount,
- getAveragePriceEstimateInHexWEI,
-} from '.';
-
-export function getGasLimit(state) {
- return state.send.gasLimit || '0';
-}
-
-export function getGasPrice(state) {
- return state.send.gasPrice || getAveragePriceEstimateInHexWEI(state);
-}
-
-export function getGasTotal(state) {
- return calcGasTotal(getGasLimit(state), getGasPrice(state));
-}
-
-export function getPrimaryCurrency(state) {
- const sendToken = getSendToken(state);
- return sendToken?.symbol;
-}
-
-export function getSendToken(state) {
- return state.send.token;
-}
-
-export function getSendTokenAddress(state) {
- return getSendToken(state)?.address;
-}
-
-export function getSendTokenContract(state) {
- const sendTokenAddress = getSendTokenAddress(state);
- return sendTokenAddress
- ? global.eth.contract(abi).at(sendTokenAddress)
- : null;
-}
-
-export function getSendAmount(state) {
- return state.send.amount;
-}
-
-export function getSendHexData(state) {
- return state.send.data;
-}
-
-export function getSendEditingTransactionId(state) {
- return state.send.editingTransactionId;
-}
-
-export function getSendErrors(state) {
- return state.send.errors;
-}
-
-export function sendAmountIsInError(state) {
- return Boolean(state.send.errors.amount);
-}
-
-export function getSendFrom(state) {
- return state.send.from;
-}
-
-export function getSendFromBalance(state) {
- const fromAccount = getSendFromObject(state);
- return fromAccount.balance;
-}
-
-export function getSendFromObject(state) {
- const fromAddress = getSendFrom(state);
- return fromAddress
- ? getTargetAccount(state, fromAddress)
- : getSelectedAccount(state);
-}
-
-export function getSendMaxModeState(state) {
- return state.send.maxModeOn;
-}
-
-export function getSendTo(state) {
- return state.send.to;
-}
-
-export function getSendToNickname(state) {
- return state.send.toNickname;
-}
-
-export function getTokenBalance(state) {
- return state.send.tokenBalance;
-}
-
-export function getSendEnsResolution(state) {
- return state.send.ensResolution;
-}
-
-export function getSendEnsResolutionError(state) {
- return state.send.ensResolutionError;
-}
-
-export function getQrCodeData(state) {
- return state.appState.qrCodeData;
-}
-
-export function getGasLoadingError(state) {
- return state.send.errors.gasLoading;
-}
-
-export function gasFeeIsInError(state) {
- return Boolean(state.send.errors.gasFee);
-}
-
-export function getGasButtonGroupShown(state) {
- return state.send.gasButtonGroupShown;
-}
-
-export function getTitleKey(state) {
- const isEditing = Boolean(getSendEditingTransactionId(state));
- const isToken = Boolean(getSendToken(state));
-
- if (!getSendTo(state)) {
- return 'addRecipient';
- }
-
- if (isEditing) {
- return 'edit';
- } else if (isToken) {
- return 'sendTokens';
- }
- return 'send';
-}
-
-export function isSendFormInError(state) {
- return Object.values(getSendErrors(state)).some((n) => n);
-}
diff --git a/ui/selectors/send.test.js b/ui/selectors/send.test.js
deleted file mode 100644
index aadbc28e5..000000000
--- a/ui/selectors/send.test.js
+++ /dev/null
@@ -1,417 +0,0 @@
-import sinon from 'sinon';
-import {
- getGasLimit,
- getGasPrice,
- getGasTotal,
- getPrimaryCurrency,
- getSendToken,
- getSendTokenContract,
- getSendAmount,
- sendAmountIsInError,
- getSendEditingTransactionId,
- getSendErrors,
- getSendFrom,
- getSendFromBalance,
- getSendFromObject,
- getSendMaxModeState,
- getSendTo,
- getTokenBalance,
- gasFeeIsInError,
- getGasLoadingError,
- getGasButtonGroupShown,
- getTitleKey,
- isSendFormInError,
-} from './send';
-import mockState from './send-selectors-test-data';
-import {
- accountsWithSendEtherInfoSelector,
- getCurrentAccountWithSendEtherInfo,
-} from '.';
-
-describe('send selectors', () => {
- const tempGlobalEth = { ...global.eth };
- beforeEach(() => {
- global.eth = {
- contract: sinon.stub().returns({
- at: (address) => `mockAt:${address}`,
- }),
- };
- });
-
- afterEach(() => {
- global.eth = tempGlobalEth;
- });
-
- describe('accountsWithSendEtherInfoSelector()', () => {
- it('should return an array of account objects with name info from identities', () => {
- expect(accountsWithSendEtherInfoSelector(mockState)).toStrictEqual([
- {
- code: '0x',
- balance: '0x47c9d71831c76efe',
- nonce: '0x1b',
- address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825',
- name: 'Send Account 1',
- },
- {
- code: '0x',
- balance: '0x37452b1315889f80',
- nonce: '0xa',
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- name: 'Send Account 2',
- },
- {
- code: '0x',
- balance: '0x30c9d71831c76efe',
- nonce: '0x1c',
- address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d',
- name: 'Send Account 3',
- },
- {
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- },
- ]);
- });
- });
-
- describe('getCurrentAccountWithSendEtherInfo()', () => {
- it('should return the currently selected account with identity info', () => {
- expect(getCurrentAccountWithSendEtherInfo(mockState)).toStrictEqual({
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- name: 'Send Account 4',
- });
- });
- });
-
- describe('getGasLimit()', () => {
- it('should return the send.gasLimit', () => {
- expect(getGasLimit(mockState)).toStrictEqual('0xFFFF');
- });
- });
-
- describe('getGasPrice()', () => {
- it('should return the send.gasPrice', () => {
- expect(getGasPrice(mockState)).toStrictEqual('0xaa');
- });
- });
-
- describe('getGasTotal()', () => {
- it('should return the send.gasTotal', () => {
- expect(getGasTotal(mockState)).toStrictEqual('a9ff56');
- });
- });
-
- describe('getPrimaryCurrency()', () => {
- it('should return the symbol of the send token', () => {
- expect(
- getPrimaryCurrency({
- send: { token: { symbol: 'DEF' } },
- }),
- ).toStrictEqual('DEF');
- });
- });
-
- describe('getSendToken()', () => {
- it('should return the current send token if set', () => {
- expect(
- getSendToken({
- send: {
- token: {
- address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'DEF',
- },
- },
- }),
- ).toStrictEqual({
- address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'DEF',
- });
- });
- });
-
- describe('getSendTokenContract()', () => {
- it('should return the contract at the send token address', () => {
- expect(
- getSendTokenContract({
- send: {
- token: {
- address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
- decimals: 4,
- symbol: 'DEF',
- },
- },
- }),
- ).toStrictEqual('mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3');
- });
-
- it('should return null if send token is not set', () => {
- expect(getSendTokenContract({ ...mockState, send: {} })).toBeNull();
- });
- });
-
- describe('getSendAmount()', () => {
- it('should return the send.amount', () => {
- expect(getSendAmount(mockState)).toStrictEqual('0x080');
- });
- });
-
- describe('getSendEditingTransactionId()', () => {
- it('should return the send.editingTransactionId', () => {
- expect(getSendEditingTransactionId(mockState)).toStrictEqual(97531);
- });
- });
-
- describe('getSendErrors()', () => {
- it('should return the send.errors', () => {
- expect(getSendErrors(mockState)).toStrictEqual({ someError: null });
- });
- });
-
- describe('getSendFrom()', () => {
- it('should return the send.from', () => {
- expect(getSendFrom(mockState)).toStrictEqual(
- '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- );
- });
- });
-
- describe('getSendFromBalance()', () => {
- it('should get the send.from balance if it exists', () => {
- expect(getSendFromBalance(mockState)).toStrictEqual('0x37452b1315889f80');
- });
-
- it('should get the selected account balance if the send.from does not exist', () => {
- const editedMockState = {
- ...mockState,
- send: {
- ...mockState.send,
- from: null,
- },
- };
- expect(getSendFromBalance(editedMockState)).toStrictEqual('0x0');
- });
- });
-
- describe('getSendFromObject()', () => {
- it('should return send.from if it exists', () => {
- expect(getSendFromObject(mockState)).toStrictEqual({
- address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- balance: '0x37452b1315889f80',
- code: '0x',
- nonce: '0xa',
- });
- });
-
- it('should return the current account if send.from does not exist', () => {
- const editedMockState = {
- ...mockState,
- send: {
- from: null,
- },
- };
- expect(getSendFromObject(editedMockState)).toStrictEqual({
- code: '0x',
- balance: '0x0',
- nonce: '0x0',
- address: '0xd85a4b6a394794842887b8284293d69163007bbb',
- });
- });
- });
-
- describe('getSendMaxModeState()', () => {
- it('should return send.maxModeOn', () => {
- expect(getSendMaxModeState(mockState)).toStrictEqual(false);
- });
- });
-
- describe('getSendTo()', () => {
- it('should return send.to', () => {
- expect(getSendTo(mockState)).toStrictEqual('0x987fedabc');
- });
- });
-
- describe('getTokenBalance()', () => {
- it('should', () => {
- expect(getTokenBalance(mockState)).toStrictEqual(3434);
- });
- });
-
- describe('send-amount-row selectors', () => {
- describe('sendAmountIsInError()', () => {
- it('should return true if send.errors.amount is truthy', () => {
- const state = {
- send: {
- errors: {
- amount: 'abc',
- },
- },
- };
-
- expect(sendAmountIsInError(state)).toStrictEqual(true);
- });
-
- it('should return false if send.errors.amount is falsy', () => {
- const state = {
- send: {
- errors: {
- amount: null,
- },
- },
- };
-
- expect(sendAmountIsInError(state)).toStrictEqual(false);
- });
- });
- });
-
- describe('send-gas-row selectors', () => {
- describe('getGasLoadingError()', () => {
- it('should return send.errors.gasLoading', () => {
- const state = {
- send: {
- errors: {
- gasLoading: 'abc',
- },
- },
- };
-
- expect(getGasLoadingError(state)).toStrictEqual('abc');
- });
- });
-
- describe('gasFeeIsInError()', () => {
- it('should return true if send.errors.gasFee is truthy', () => {
- const state = {
- send: {
- errors: {
- gasFee: 'def',
- },
- },
- };
-
- expect(gasFeeIsInError(state)).toStrictEqual(true);
- });
-
- it('should return false send.errors.gasFee is falsely', () => {
- const state = {
- send: {
- errors: {
- gasFee: null,
- },
- },
- };
-
- expect(gasFeeIsInError(state)).toStrictEqual(false);
- });
- });
-
- describe('getGasButtonGroupShown()', () => {
- it('should return send.gasButtonGroupShown', () => {
- const state = {
- send: {
- gasButtonGroupShown: 'foobar',
- },
- };
-
- expect(getGasButtonGroupShown(state)).toStrictEqual('foobar');
- });
- });
- });
-
- describe('send-header selectors', () => {
- const getMetamaskSendMockState = (send) => {
- return {
- send: { ...send },
- };
- };
-
- describe('getTitleKey()', () => {
- it('should return the correct key when "to" is empty', () => {
- expect(getTitleKey(getMetamaskSendMockState({}))).toStrictEqual(
- 'addRecipient',
- );
- });
-
- it('should return the correct key when getSendEditingTransactionId is truthy', () => {
- expect(
- getTitleKey(
- getMetamaskSendMockState({
- to: true,
- editingTransactionId: true,
- token: {},
- }),
- ),
- ).toStrictEqual('edit');
- });
-
- it('should return the correct key when getSendEditingTransactionId is falsy and getSendToken is truthy', () => {
- expect(
- getTitleKey(
- getMetamaskSendMockState({
- to: true,
- editingTransactionId: false,
- token: {},
- }),
- ),
- ).toStrictEqual('sendTokens');
- });
-
- it('should return the correct key when getSendEditingTransactionId is falsy and getSendToken is falsy', () => {
- expect(
- getTitleKey(
- getMetamaskSendMockState({
- to: true,
- editingTransactionId: false,
- token: null,
- }),
- ),
- ).toStrictEqual('send');
- });
- });
- });
-
- describe('send-footer selectors', () => {
- const getSendMockState = (send) => {
- return {
- send: { ...send },
- };
- };
-
- describe('isSendFormInError()', () => {
- it('should return true if any of the values of the object returned by getSendErrors are truthy', () => {
- expect(
- isSendFormInError(
- getSendMockState({
- errors: [true],
- }),
- ),
- ).toStrictEqual(true);
- });
-
- it('should return false if all of the values of the object returned by getSendErrors are falsy', () => {
- expect(
- isSendFormInError(
- getSendMockState({
- errors: [],
- }),
- ),
- ).toStrictEqual(false);
- expect(
- isSendFormInError(
- getSendMockState({
- errors: [false],
- }),
- ),
- ).toStrictEqual(false);
- });
- });
- });
-});
diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js
index d16caa61f..c96088f2f 100644
--- a/ui/store/actionConstants.js
+++ b/ui/store/actionConstants.js
@@ -15,6 +15,10 @@ export const NETWORK_DROPDOWN_CLOSE = 'UI_NETWORK_DROPDOWN_CLOSE';
// remote state
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
+export const SELECTED_ACCOUNT_CHANGED = 'SELECTED_ACCOUNT_CHANGED';
+export const ACCOUNT_CHANGED = 'ACCOUNT_CHANGED';
+export const CHAIN_CHANGED = 'CHAIN_CHANGED';
+export const ADDRESS_BOOK_UPDATED = 'ADDRESS_BOOK_UPDATED';
export const FORGOT_PASSWORD = 'FORGOT_PASSWORD';
export const CLOSE_WELCOME_SCREEN = 'CLOSE_WELCOME_SCREEN';
// unlock screen
@@ -60,7 +64,6 @@ export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE';
export const SET_IPFS_GATEWAY = 'SET_IPFS_GATEWAY';
export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS';
-export const SET_METAMETRICS_SEND_COUNT = 'SET_METAMETRICS_SEND_COUNT';
// locale
export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE';
diff --git a/ui/store/actionConstants.test.js b/ui/store/actionConstants.test.js
index 7cfef827f..eba555a51 100644
--- a/ui/store/actionConstants.test.js
+++ b/ui/store/actionConstants.test.js
@@ -63,9 +63,7 @@ describe('Redux actionConstants', () => {
describe('SHOW_ACCOUNT_DETAIL', () => {
it('updates metamask state', () => {
const initialState = {
- metamask: {
- selectedAddress: 'foo',
- },
+ metamask: {},
};
freeze(initialState);
@@ -76,9 +74,8 @@ describe('Redux actionConstants', () => {
freeze(action);
const resultingState = reducers(initialState, action);
- expect(resultingState.metamask.selectedAddress).toStrictEqual(
- action.value,
- );
+ expect(resultingState.metamask.isUnlocked).toStrictEqual(true);
+ expect(resultingState.metamask.isInitialized).toStrictEqual(true);
});
});
});
diff --git a/ui/store/actions.js b/ui/store/actions.js
index f2269cdad..e8f97eedd 100644
--- a/ui/store/actions.js
+++ b/ui/store/actions.js
@@ -1,7 +1,6 @@
-import abi from 'human-standard-token-abi';
import pify from 'pify';
import log from 'loglevel';
-import { capitalize } from 'lodash';
+import { capitalize, isEqual } from 'lodash';
import getBuyEthUrl from '../../app/scripts/lib/buy-eth-url';
import {
fetchLocale,
@@ -15,14 +14,15 @@ import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
import txHelper from '../helpers/utils/tx-helper';
import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util';
import {
+ getMetaMaskAccounts,
getPermittedAccountsForCurrentTab,
getSelectedAddress,
} from '../selectors';
+import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
-import { clearSend } from '../ducks/send/send.duck';
import * as actionConstants from './actionConstants';
let background = null;
@@ -621,19 +621,6 @@ export function signTypedMsg(msgData) {
};
}
-export function signTx(txData) {
- return async (dispatch) => {
- dispatch(showLoadingIndication());
- global.ethQuery.sendTransaction(txData, (err) => {
- if (err) {
- dispatch(displayWarning(err.message));
- }
- });
- dispatch(hideLoadingIndication());
- dispatch(showConfTxPage());
- };
-}
-
export function updateCustomNonce(value) {
return {
type: actionConstants.UPDATE_CUSTOM_NONCE,
@@ -641,22 +628,6 @@ export function updateCustomNonce(value) {
};
}
-export function signTokenTx(tokenAddress, toAddress, amount, txData) {
- return async (dispatch) => {
- dispatch(showLoadingIndication());
-
- try {
- const token = global.eth.contract(abi).at(tokenAddress);
- token.transfer(toAddress, addHexPrefix(amount), txData);
- dispatch(showConfTxPage());
- dispatch(hideLoadingIndication());
- } catch (error) {
- dispatch(hideLoadingIndication());
- dispatch(displayWarning(error.message));
- }
- };
-}
-
const updateMetamaskStateFromBackground = () => {
log.debug(`background.getState`);
@@ -721,7 +692,7 @@ export function updateAndApproveTx(txData, dontShowLoadingIndicator) {
return new Promise((resolve, reject) => {
background.updateAndApproveTransaction(txData, (err) => {
dispatch(updateTransactionParams(txData.id, txData.txParams));
- dispatch(clearSend());
+ dispatch(resetSendState());
if (err) {
dispatch(txError(err));
@@ -737,7 +708,7 @@ export function updateAndApproveTx(txData, dontShowLoadingIndicator) {
.then(() => updateMetamaskStateFromBackground())
.then((newState) => dispatch(updateMetamaskState(newState)))
.then(() => {
- dispatch(clearSend());
+ dispatch(resetSendState());
dispatch(completedTx(txData.id));
dispatch(hideLoadingIndication());
dispatch(updateCustomNonce(''));
@@ -907,7 +878,7 @@ export function cancelTx(txData, _showLoadingIndication = true) {
.then(() => updateMetamaskStateFromBackground())
.then((newState) => dispatch(updateMetamaskState(newState)))
.then(() => {
- dispatch(clearSend());
+ dispatch(resetSendState());
dispatch(completedTx(txData.id));
dispatch(hideLoadingIndication());
dispatch(closeCurrentNotificationWindow());
@@ -950,7 +921,7 @@ export function cancelTxs(txDataList) {
const newState = await updateMetamaskStateFromBackground();
dispatch(updateMetamaskState(newState));
- dispatch(clearSend());
+ dispatch(resetSendState());
txIds.forEach((id) => {
dispatch(completedTx(id));
@@ -1038,19 +1009,59 @@ export function updateMetamaskState(newState) {
return (dispatch, getState) => {
const { metamask: currentState } = getState();
- const { currentLocale, selectedAddress } = currentState;
+ const { currentLocale, selectedAddress, provider } = currentState;
const {
currentLocale: newLocale,
selectedAddress: newSelectedAddress,
+ provider: newProvider,
} = newState;
if (currentLocale && newLocale && currentLocale !== newLocale) {
dispatch(updateCurrentLocale(newLocale));
}
+
if (selectedAddress !== newSelectedAddress) {
dispatch({ type: actionConstants.SELECTED_ADDRESS_CHANGED });
}
+ const newAddressBook = newState.addressBook?.[newProvider?.chainId] ?? {};
+ const oldAddressBook = currentState.addressBook?.[provider?.chainId] ?? {};
+ const newAccounts = getMetaMaskAccounts({ metamask: newState });
+ const oldAccounts = getMetaMaskAccounts({ metamask: currentState });
+ const newSelectedAccount = newAccounts[newSelectedAddress];
+ const oldSelectedAccount = newAccounts[selectedAddress];
+ // dispatch an ACCOUNT_CHANGED for any account whose balance or other
+ // properties changed in this update
+ Object.entries(oldAccounts).forEach(([address, oldAccount]) => {
+ if (!isEqual(oldAccount, newAccounts[address])) {
+ dispatch({
+ type: actionConstants.ACCOUNT_CHANGED,
+ payload: { account: newAccounts[address] },
+ });
+ }
+ });
+ // Also emit an event for the selected account changing, either due to a
+ // property update or if the entire account changes.
+ if (isEqual(oldSelectedAccount, newSelectedAccount) === false) {
+ dispatch({
+ type: actionConstants.SELECTED_ACCOUNT_CHANGED,
+ payload: { account: newSelectedAccount },
+ });
+ }
+ // We need to keep track of changing address book entries
+ if (isEqual(oldAddressBook, newAddressBook) === false) {
+ dispatch({
+ type: actionConstants.ADDRESS_BOOK_UPDATED,
+ payload: { addressBook: newAddressBook },
+ });
+ }
+
+ if (provider.chainId !== newProvider.chainId) {
+ dispatch({
+ type: actionConstants.CHAIN_CHANGED,
+ payload: newProvider.chainId,
+ });
+ }
dispatch({
type: actionConstants.UPDATE_METAMASK_STATE,
value: newState,
@@ -1141,6 +1152,7 @@ export function showAccountDetail(address) {
try {
await _setSelectedAddress(dispatch, address);
+ await forceUpdateMetamaskState(dispatch);
} catch (error) {
dispatch(displayWarning(error.message));
return;
@@ -1657,9 +1669,16 @@ export function hideAlert() {
* or null (used to clear the previous value)
*/
export function qrCodeDetected(qrCodeData) {
- return {
- type: actionConstants.QR_CODE_DETECTED,
- value: qrCodeData,
+ return async (dispatch) => {
+ await dispatch({
+ type: actionConstants.QR_CODE_DETECTED,
+ value: qrCodeData,
+ });
+
+ // If on the send page, the send slice will listen for the QR_CODE_DETECTED
+ // action and update its state. Address changes need to recompute gasLimit
+ // so we fire this method so that the send page gasLimit can be recomputed
+ dispatch(computeEstimatedGasLimit());
};
}
@@ -1807,8 +1826,8 @@ export function showSendTokenPage() {
}
export function buyEth(opts) {
- return (dispatch) => {
- const url = getBuyEthUrl(opts);
+ return async (dispatch) => {
+ const url = await getBuyEthUrl(opts);
global.platform.openTab({ url });
dispatch({
type: actionConstants.BUY_ETH,
@@ -1968,27 +1987,6 @@ export function setParticipateInMetaMetrics(val) {
};
}
-export function setMetaMetricsSendCount(val) {
- return (dispatch) => {
- log.debug(`background.setMetaMetricsSendCount`);
- return new Promise((resolve, reject) => {
- background.setMetaMetricsSendCount(val, (err) => {
- if (err) {
- dispatch(displayWarning(err.message));
- reject(err);
- return;
- }
-
- dispatch({
- type: actionConstants.SET_METAMETRICS_SEND_COUNT,
- value: val,
- });
- resolve(val);
- });
- });
- };
-}
-
export function setUseBlockie(val) {
return (dispatch) => {
dispatch(showLoadingIndication());
@@ -2704,6 +2702,16 @@ export function estimateGas(params) {
return promisifiedBackground.estimateGas(params);
}
+export async function updateTokenType(tokenAddress) {
+ let token = {};
+ try {
+ token = await promisifiedBackground.updateTokenType(tokenAddress);
+ } catch (error) {
+ log.error(error);
+ }
+ return token;
+}
+
// MetaMetrics
/**
* @typedef {import('../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js
index d28defb8c..847141fd5 100644
--- a/ui/store/actions.test.js
+++ b/ui/store/actions.test.js
@@ -1,7 +1,6 @@
import sinon from 'sinon';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
-import EthQuery from 'eth-query';
import enLocale from '../../app/_locales/en/messages.json';
import MetaMaskController from '../../app/scripts/metamask-controller';
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
@@ -14,10 +13,22 @@ const defaultState = {
currentLocale: 'test',
selectedAddress: '0xFirstAddress',
provider: { chainId: '0x1' },
+ accounts: {
+ '0xFirstAddress': {
+ balance: '0x0',
+ },
+ },
+ cachedBalances: {
+ '0x1': {
+ '0xFirstAddress': '0x0',
+ },
+ },
},
};
const mockStore = (state = defaultState) => configureStore(middleware)(state);
+const baseMockState = defaultState.metamask;
+
describe('Actions', () => {
let background;
@@ -25,12 +36,7 @@ describe('Actions', () => {
beforeEach(async () => {
background = sinon.createStubInstance(MetaMaskController, {
- getState: sinon.stub().callsFake((cb) =>
- cb(null, {
- currentLocale: 'test',
- selectedAddress: '0xFirstAddress',
- }),
- ),
+ getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
});
@@ -58,10 +64,7 @@ describe('Actions', () => {
{ type: 'UNLOCK_SUCCEEDED', value: undefined },
{
type: 'UPDATE_METAMASK_STATE',
- value: {
- currentLocale: 'test',
- selectedAddress: '0xFirstAddress',
- },
+ value: baseMockState,
},
{ type: 'HIDE_LOADING_INDICATION' },
];
@@ -111,7 +114,7 @@ describe('Actions', () => {
{ type: 'UNLOCK_SUCCEEDED', value: undefined },
{
type: 'UPDATE_METAMASK_STATE',
- value: { currentLocale: 'test', selectedAddress: '0xFirstAddress' },
+ value: baseMockState,
},
{ type: 'DISPLAY_WARNING', value: 'error' },
{ type: 'UNLOCK_FAILED', value: 'error' },
@@ -159,10 +162,7 @@ describe('Actions', () => {
{ type: 'FORGOT_PASSWORD', value: false },
{
type: 'UPDATE_METAMASK_STATE',
- value: {
- currentLocale: 'test',
- selectedAddress: '0xFirstAddress',
- },
+ value: baseMockState,
},
{ type: 'SHOW_ACCOUNTS_PAGE' },
{ type: 'HIDE_LOADING_INDICATION' },
@@ -254,6 +254,19 @@ describe('Actions', () => {
cb(null, {
currentLocale: 'test',
selectedAddress: '0xAnotherAddress',
+ provider: {
+ chainId: '0x1',
+ },
+ accounts: {
+ '0xAnotherAddress': {
+ balance: '0x0',
+ },
+ },
+ cachedBalances: {
+ '0x1': {
+ '0xAnotherAddress': '0x0',
+ },
+ },
}),
);
@@ -264,6 +277,8 @@ describe('Actions', () => {
const expectedActions = [
'SHOW_LOADING_INDICATION',
'SELECTED_ADDRESS_CHANGED',
+ 'ACCOUNT_CHANGED',
+ 'SELECTED_ACCOUNT_CHANGED',
'UPDATE_METAMASK_STATE',
'HIDE_LOADING_INDICATION',
'SHOW_ACCOUNTS_PAGE',
@@ -400,7 +415,9 @@ describe('Actions', () => {
describe('#addNewAccount', () => {
it('adds a new account', async () => {
- const store = mockStore({ metamask: { identities: {} } });
+ const store = mockStore({
+ metamask: { identities: {}, ...defaultState.metamask },
+ });
const addNewAccount = background.addNewAccount.callsFake((cb) =>
cb(null, {
@@ -660,7 +677,7 @@ describe('Actions', () => {
const store = mockStore();
const signMessage = background.signMessage.callsFake((_, cb) =>
- cb(null, defaultState),
+ cb(null, defaultState.metamask),
);
actions._setBackgroundConnection(background);
@@ -705,7 +722,7 @@ describe('Actions', () => {
const store = mockStore();
const signPersonalMessage = background.signPersonalMessage.callsFake(
- (_, cb) => cb(null, defaultState),
+ (_, cb) => cb(null, defaultState.metamask),
);
actions._setBackgroundConnection(background);
@@ -786,7 +803,7 @@ describe('Actions', () => {
const store = mockStore();
const signTypedMsg = background.signTypedMessage.callsFake((_, cb) =>
- cb(null, defaultState),
+ cb(null, defaultState.metamask),
);
actions._setBackgroundConnection(background);
@@ -816,58 +833,6 @@ describe('Actions', () => {
});
});
- describe('#signTx', () => {
- beforeEach(() => {
- global.ethQuery = sinon.createStubInstance(EthQuery);
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it('calls sendTransaction in global ethQuery', async () => {
- const store = mockStore();
-
- actions._setBackgroundConnection(background);
-
- await store.dispatch(actions.signTx());
-
- expect(global.ethQuery.sendTransaction.callCount).toStrictEqual(1);
- });
-
- it('errors in when sendTransaction throws', async () => {
- const store = mockStore();
- const expectedActions = [
- { type: 'SHOW_LOADING_INDICATION', value: undefined },
- { type: 'DISPLAY_WARNING', value: 'error' },
- { type: 'HIDE_LOADING_INDICATION' },
- { type: 'SHOW_CONF_TX_PAGE', id: undefined },
- ];
-
- global.ethQuery.sendTransaction.callsFake((_, callback) => {
- callback(new Error('error'));
- });
-
- actions._setBackgroundConnection(background);
-
- await store.dispatch(actions.signTx());
- expect(store.getActions()).toStrictEqual(expectedActions);
- });
- });
-
- describe('#signTokenTx', () => {
- it('calls eth.contract', async () => {
- global.eth = {
- contract: sinon.stub(),
- };
-
- const store = mockStore();
-
- await store.dispatch(actions.signTokenTx());
- expect(global.eth.contract.callCount).toStrictEqual(1);
- });
- });
-
describe('#updateTransaction', () => {
const txParams = {
from: '0x1',
@@ -895,12 +860,7 @@ describe('Actions', () => {
background.getApi.returns({
updateTransaction: updateTransactionStub,
- getState: sinon.stub().callsFake((cb) =>
- cb(null, {
- currentLocale: 'test',
- selectedAddress: '0xFirstAddress',
- }),
- ),
+ getState: sinon.stub().callsFake((cb) => cb(null, baseMockState)),
});
actions._setBackgroundConnection(background.getApi());
@@ -1699,10 +1659,7 @@ describe('Actions', () => {
{ type: 'FORGOT_PASSWORD', value: true },
{
type: 'UPDATE_METAMASK_STATE',
- value: {
- currentLocale: 'test',
- selectedAddress: '0xFirstAddress',
- },
+ value: baseMockState,
},
];
@@ -1762,6 +1719,19 @@ describe('Actions', () => {
cb(null, {
currentLocale: 'test',
selectedAddress: '0xFirstAddress',
+ provider: {
+ chainId: '0x1',
+ },
+ accounts: {
+ '0xFirstAddress': {
+ balance: '0x0',
+ },
+ },
+ cachedBalances: {
+ '0x1': {
+ '0xFirstAddress': '0x0',
+ },
+ },
}),
),
});
diff --git a/yarn.lock b/yarn.lock
index c5537275a..430a702bd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1521,6 +1521,22 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@ethereumjs/common@^2.3.1":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.3.1.tgz#d692e3aff5adb35dd587dd1e6caab69e0ed2fa0b"
+ integrity sha512-V8hrULExoq0H4HFs3cCmdRGbgmipmlNzak6Xg34nHYfQyqkSdrCuflvYjyWmsNpI8GtrcZhzifAbgX/1C1Cjwg==
+ dependencies:
+ crc-32 "^1.2.0"
+ ethereumjs-util "^7.0.10"
+
+"@ethereumjs/tx@^3.1.1", "@ethereumjs/tx@^3.1.4", "@ethereumjs/tx@^3.2.1":
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.2.1.tgz#65f5f1c11541764f08377a94ba4b0dcbbd67739e"
+ integrity sha512-i9V39OtKvwWos1uVNZxdVhd7zFOyzFLjgt69CoiOY0EmXugS0HjO3uxpLBSglDKFMRriuGqw6ddKEv+RP1UNEw==
+ dependencies:
+ "@ethereumjs/common" "^2.3.1"
+ ethereumjs-util "^7.0.10"
+
"@ethersproject/abi@5.0.0-beta.153":
version "5.0.0-beta.153"
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.0-beta.153.tgz#43a37172b33794e4562999f6e2d555b7599a8eee"
@@ -2640,6 +2656,39 @@
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.26.0.tgz#06be4f4dc645da69f6364f75cb2bd47afa642fe6"
integrity sha512-58A8csapIPoc854n6AI+3jwJcQfh75BzVrl6SAySgJ9fWTa1XItm9Tx/ORaqYrwaR/9JqH4SnkbXtqyFwuUL6w==
+"@metamask/controllers@^10.0.0":
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-10.1.0.tgz#ec0a3751fa658966e9818038784ab6b555c95382"
+ integrity sha512-Me0jzI5CIOdfXkpm3eBOEIxDGlgbKQaW1au0GQdVgbW93ZxDueTqLUg9xSGSfGSJ3i+Alfqi/tnGqT/nwa/5CQ==
+ dependencies:
+ "@metamask/contract-metadata" "^1.25.0"
+ "@types/uuid" "^8.3.0"
+ async-mutex "^0.2.6"
+ babel-runtime "^6.26.0"
+ eth-ens-namehash "^2.0.8"
+ eth-json-rpc-infura "^5.1.0"
+ eth-keyring-controller "^6.2.1"
+ eth-method-registry "1.1.0"
+ eth-phishing-detect "^1.1.14"
+ eth-query "^2.1.2"
+ eth-rpc-errors "^4.0.0"
+ eth-sig-util "^3.0.0"
+ ethereumjs-tx "^1.3.7"
+ ethereumjs-util "^7.0.10"
+ ethereumjs-wallet "^1.0.1"
+ ethjs-util "^0.1.6"
+ human-standard-collectible-abi "^1.0.2"
+ human-standard-token-abi "^2.0.0"
+ immer "^8.0.1"
+ isomorphic-fetch "^3.0.0"
+ jsonschema "^1.2.4"
+ nanoid "^3.1.12"
+ punycode "^2.1.1"
+ single-call-balance-checker-abi "^1.0.0"
+ uuid "^8.3.2"
+ web3 "^0.20.7"
+ web3-provider-engine "^16.0.1"
+
"@metamask/controllers@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-5.1.0.tgz#02c1957295bcb6db1655a716d165665d170e7f34"
@@ -2668,39 +2717,6 @@
web3 "^0.20.7"
web3-provider-engine "^16.0.1"
-"@metamask/controllers@^9.0.0":
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-9.1.0.tgz#4434f22eba2522889224b35aa08bc7b67d7248b7"
- integrity sha512-jn/F0BNbaPsgEevHaPqk0lGAONKom4re1a4yBC67h7Vu6yu26CRi30SJl4xIh3IW4+ySbPhVLaiXFiXr3fESRQ==
- dependencies:
- "@metamask/contract-metadata" "^1.25.0"
- "@types/uuid" "^8.3.0"
- async-mutex "^0.2.6"
- babel-runtime "^6.26.0"
- eth-ens-namehash "^2.0.8"
- eth-json-rpc-infura "^5.1.0"
- eth-keyring-controller "^6.2.1"
- eth-method-registry "1.1.0"
- eth-phishing-detect "^1.1.14"
- eth-query "^2.1.2"
- eth-rpc-errors "^4.0.0"
- eth-sig-util "^3.0.0"
- ethereumjs-tx "^1.3.7"
- ethereumjs-util "^6.1.0"
- ethereumjs-wallet "^1.0.1"
- ethjs-util "^0.1.6"
- human-standard-collectible-abi "^1.0.2"
- human-standard-token-abi "^2.0.0"
- immer "^8.0.1"
- isomorphic-fetch "^3.0.0"
- jsonschema "^1.2.4"
- nanoid "^3.1.12"
- punycode "^2.1.1"
- single-call-balance-checker-abi "^1.0.0"
- uuid "^8.3.2"
- web3 "^0.20.7"
- web3-provider-engine "^16.0.1"
-
"@metamask/eslint-config-jest@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@metamask/eslint-config-jest/-/eslint-config-jest-6.0.0.tgz#9e10cfbca31236afd7be2058be70365084e540d6"
@@ -2721,11 +2737,12 @@
resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-6.0.0.tgz#ec53e8ab278073e882411ed89705bc7d06b78c81"
integrity sha512-LyakGYGwM8UQOGhwWa+5erAI1hXuiTgf/y7USzOomX6H9KiuY09IAUYnPh7ToPG2sedD2F48UF1bUm8yvCoZOw==
-"@metamask/eth-ledger-bridge-keyring@^0.5.0":
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.5.0.tgz#c1ee89819c493239290a940da6285e6844240383"
- integrity sha512-p7dvnAQ6n9AFf7JoJFwNsdIKoX5poP0bBwrgmCA/mLAdb5Z+bBAnHMSbKOZrH7rEsj7jExdbmUiJLI5qKnV0zA==
+"@metamask/eth-ledger-bridge-keyring@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.6.0.tgz#2f1344eb76bf880c8cfbb92b9b64510920c7e335"
+ integrity sha512-eP0f8Q7o5K35wVinK2EjEw/gVY/pvzkAcbk49CwQEzGPKm6d+AAURJZ0o33/8gYGIrpq4bmWZhZzgyAbphgVLw==
dependencies:
+ "@ethereumjs/tx" "^3.1.1"
eth-sig-util "^2.0.0"
ethereumjs-tx "^1.3.4"
ethereumjs-util "^7.0.9"
@@ -8358,6 +8375,14 @@ cpy@^8.1.1:
p-filter "^2.1.0"
p-map "^3.0.0"
+crc-32@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
+ integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
+ dependencies:
+ exit-on-epipe "~1.0.1"
+ printj "~1.1.0"
+
crdts@~0.1.2:
version "0.1.5"
resolved "https://registry.yarnpkg.com/crdts/-/crdts-0.1.5.tgz#89413e8adfc3ab943300a890ee6392db5ba60c06"
@@ -10713,14 +10738,15 @@ eth-simple-keyring@^4.2.0:
ethereumjs-wallet "^1.0.1"
events "^1.1.1"
-eth-trezor-keyring@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/eth-trezor-keyring/-/eth-trezor-keyring-0.6.0.tgz#85192ab5d98be28512a58b0bc731e4e6a124ba27"
- integrity sha512-10r2uYbrNdYlzgHy2VFEyoGmGw3YIBa21WDT365cTzIQ7C6ozAvfZ9t5YZkTVY2AQP5TVS41UZJdbj0R2DG2Lg==
+eth-trezor-keyring@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/eth-trezor-keyring/-/eth-trezor-keyring-0.7.0.tgz#99ebaae6783ae40347352b36f7cf19e3b818c12d"
+ integrity sha512-WSJ/n68qsMzrMyTgfKiF2/Ytn37hWrIwQrkjBt6Tmt/btQnrQkwG/7Fv/diOUJNCG0HNGWyxQpkvtBOnckboLQ==
dependencies:
+ "@ethereumjs/tx" "^3.1.4"
eth-sig-util "^1.4.2"
ethereumjs-tx "^1.3.4"
- ethereumjs-util "^5.1.5"
+ ethereumjs-util "^7.0.9"
hdkey "0.8.0"
trezor-connect "8.1.23-extended"
@@ -10874,14 +10900,6 @@ ethereumjs-common@^1.1.0, ethereumjs-common@^1.3.2, ethereumjs-common@^1.5.0:
resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-1.5.1.tgz#4e75042473a64daec0ed9fe84323dd9576aa5dba"
integrity sha512-aVUPRLgmXORGXXEVkFYgPhr9TGtpBY2tGhZ9Uh0A3lIUzUDr1x6kQx33SbjPUkLkX3eniPQnIL/2psjkjrOfcQ==
-ethereumjs-tx@1.3.7, ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@^1.3.3, ethereumjs-tx@^1.3.4, ethereumjs-tx@^1.3.7:
- version "1.3.7"
- resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a"
- integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==
- dependencies:
- ethereum-common "^0.0.18"
- ethereumjs-util "^5.0.0"
-
ethereumjs-tx@2.1.2, ethereumjs-tx@^2.1.1, ethereumjs-tx@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz#5dfe7688bf177b45c9a23f86cf9104d47ea35fed"
@@ -10890,6 +10908,14 @@ ethereumjs-tx@2.1.2, ethereumjs-tx@^2.1.1, ethereumjs-tx@^2.1.2:
ethereumjs-common "^1.5.0"
ethereumjs-util "^6.0.0"
+ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@^1.3.3, ethereumjs-tx@^1.3.4, ethereumjs-tx@^1.3.7:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a"
+ integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==
+ dependencies:
+ ethereum-common "^0.0.18"
+ ethereumjs-util "^5.0.0"
+
ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0, ethereumjs-util@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69"
@@ -11393,6 +11419,11 @@ exit-hook@^1.0.0:
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
+exit-on-epipe@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+ integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
+
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -21012,6 +21043,11 @@ pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
+printj@~1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+ integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
+
priorityqueue@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/priorityqueue/-/priorityqueue-0.2.1.tgz#f57e623f20237f30c142d4cb45fafed9e7d51403"