aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/send_/tests
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components/send_/tests')
-rw-r--r--ui/app/components/send_/tests/send-component.test.js251
-rw-r--r--ui/app/components/send_/tests/send-container.test.js155
-rw-r--r--ui/app/components/send_/tests/send-selectors-test-data.js230
-rw-r--r--ui/app/components/send_/tests/send-selectors.test.js695
-rw-r--r--ui/app/components/send_/tests/send-utils.test.js404
5 files changed, 1735 insertions, 0 deletions
diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js
new file mode 100644
index 000000000..4e33d8f63
--- /dev/null
+++ b/ui/app/components/send_/tests/send-component.test.js
@@ -0,0 +1,251 @@
+import React from 'react'
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+
+import SendHeader from '../send-header/send-header.container'
+import SendContent from '../send-content/send-content.component'
+import SendFooter from '../send-footer/send-footer.container'
+
+const propsMethodSpies = {
+ updateAndSetGasTotal: sinon.spy(),
+ updateSendErrors: sinon.spy(),
+ updateSendTokenBalance: sinon.spy(),
+}
+const utilsMethodStubs = {
+ getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
+ doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance),
+}
+
+const SendTransactionScreen = proxyquire('../send.component.js', {
+ './send.utils': utilsMethodStubs,
+}).default
+
+sinon.spy(SendTransactionScreen.prototype, 'componentDidMount')
+sinon.spy(SendTransactionScreen.prototype, 'updateGas')
+
+describe('Send Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendTransactionScreen
+ amount={'mockAmount'}
+ amountConversionRate={'mockAmountConversionRate'}
+ blockGasLimit={'mockBlockGasLimit'}
+ conversionRate={10}
+ editingTransactionId={'mockEditingTransactionId'}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={{ mockProp: 'history-abc'}}
+ network={'3'}
+ primaryCurrency={'mockPrimaryCurrency'}
+ recentBlocks={['mockBlock']}
+ selectedAddress={'mockSelectedAddress'}
+ selectedToken={'mockSelectedToken'}
+ tokenBalance={'mockTokenBalance'}
+ tokenContract={'mockTokenContract'}
+ updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
+ updateSendErrors={propsMethodSpies.updateSendErrors}
+ updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
+ />)
+ })
+
+ afterEach(() => {
+ SendTransactionScreen.prototype.componentDidMount.resetHistory()
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ propsMethodSpies.updateAndSetGasTotal.resetHistory()
+ propsMethodSpies.updateSendErrors.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ })
+
+ it('should call componentDidMount', () => {
+ assert(SendTransactionScreen.prototype.componentDidMount.calledOnce)
+ })
+
+ describe('componentWillMount', () => {
+ it('should call this.updateGas', () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendErrors.resetHistory()
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ wrapper.instance().componentWillMount()
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
+ })
+ })
+
+ describe('componentDidUpdate', () => {
+ it('should call doesAmountErrorRequireUpdate with the expected params', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: '',
+ },
+ })
+ assert(utilsMethodStubs.doesAmountErrorRequireUpdate.calledOnce)
+ assert.deepEqual(
+ utilsMethodStubs.doesAmountErrorRequireUpdate.getCall(0).args[0],
+ {
+ balance: 'mockBalance',
+ gasTotal: 'mockGasTotal',
+ prevBalance: '',
+ prevGasTotal: undefined,
+ prevTokenBalance: undefined,
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'mockBalance',
+ },
+ })
+ assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 0)
+ })
+
+ it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => {
+ utilsMethodStubs.getAmountErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 1)
+ assert.deepEqual(
+ utilsMethodStubs.getAmountErrorObject.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 10,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should call updateSendErrors with the expected params', () => {
+ propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendErrors.getCall(0).args[0],
+ { amount: 'mockAmountError'}
+ )
+ })
+
+ it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '3',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ })
+
+ it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => {
+ wrapper.setProps({ network: 'loading' })
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '3',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0)
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0)
+ })
+
+ it('should call updateSendTokenBalance and this.updateGas with the correct params', () => {
+ SendTransactionScreen.prototype.updateGas.resetHistory()
+ propsMethodSpies.updateSendTokenBalance.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ network: '2',
+ })
+ assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendTokenBalance.getCall(0).args[0],
+ {
+ selectedToken: 'mockSelectedToken',
+ tokenContract: 'mockTokenContract',
+ address: 'mockAddress',
+ }
+ )
+ assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1)
+ assert.deepEqual(
+ SendTransactionScreen.prototype.updateGas.getCall(0).args,
+ []
+ )
+ })
+ })
+
+ describe('updateGas', () => {
+ it('should call updateAndSetGasTotal with the correct params', () => {
+ propsMethodSpies.updateAndSetGasTotal.resetHistory()
+ wrapper.instance().updateGas()
+ assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0],
+ {
+ blockGasLimit: 'mockBlockGasLimit',
+ editingTransactionId: 'mockEditingTransactionId',
+ gasLimit: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ recentBlocks: ['mockBlock'],
+ selectedAddress: 'mockSelectedAddress',
+ selectedToken: 'mockSelectedToken',
+ to: undefined,
+ value: 'mockAmount',
+ }
+ )
+ })
+
+ it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
+ propsMethodSpies.updateAndSetGasTotal.resetHistory()
+ wrapper.instance().updateGas({ to: '0xABC' })
+ assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc')
+ })
+ })
+
+ describe('render', () => {
+ it('should render a page-container class', () => {
+ assert.equal(wrapper.find('.page-container').length, 1)
+ })
+
+ it('should render SendHeader, SendContent and SendFooter', () => {
+ assert.equal(wrapper.find(SendHeader).length, 1)
+ assert.equal(wrapper.find(SendContent).length, 1)
+ assert.equal(wrapper.find(SendFooter).length, 1)
+ })
+
+ it('should pass the history prop to SendHeader and SendFooter', () => {
+ assert.deepEqual(
+ wrapper.find(SendFooter).props(),
+ {
+ history: { mockProp: 'history-abc' },
+ }
+ )
+ })
+ })
+})
diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js
new file mode 100644
index 000000000..056aad148
--- /dev/null
+++ b/ui/app/components/send_/tests/send-container.test.js
@@ -0,0 +1,155 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ updateSendTokenBalance: sinon.spy(),
+ updateGasData: sinon.spy(),
+ setGasTotal: sinon.spy(),
+}
+const duckActionSpies = {
+ updateSendErrors: sinon.spy(),
+}
+
+proxyquire('../send.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ 'react-router-dom': { withRouter: () => {} },
+ 'recompose': { compose: (arg1, arg2) => () => arg2() },
+ './send.selectors': {
+ getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
+ getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`,
+ getConversionRate: (s) => `mockConversionRate:${s}`,
+ getCurrentNetwork: (s) => `mockNetwork:${s}`,
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
+ getRecentBlocks: (s) => `mockRecentBlocks:${s}`,
+ getSelectedAddress: (s) => `mockSelectedAddress:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
+ getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`,
+ getSendAmount: (s) => `mockAmount:${s}`,
+ getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
+ getSendFromObject: (s) => `mockFrom:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ },
+ '../../actions': actionSpies,
+ '../../ducks/send.duck': duckActionSpies,
+ './send.utils.js': {
+ calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
+ },
+})
+
+describe('send container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ amount: 'mockAmount:mockState',
+ amountConversionRate: 'mockAmountConversionRate:mockState',
+ blockGasLimit: 'mockBlockGasLimit:mockState',
+ conversionRate: 'mockConversionRate:mockState',
+ editingTransactionId: 'mockEditingTransactionId:mockState',
+ from: 'mockFrom:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ network: 'mockNetwork:mockState',
+ primaryCurrency: 'mockPrimaryCurrency:mockState',
+ recentBlocks: 'mockRecentBlocks:mockState',
+ selectedAddress: 'mockSelectedAddress:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ tokenContract: 'mockTokenContract:mockState',
+ tokenToFiatRate: 'mockTokenToFiatRate:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('updateAndSetGasTotal()', () => {
+ const mockProps = {
+ blockGasLimit: 'mockBlockGasLimit',
+ editingTransactionId: '0x2',
+ gasLimit: '0x3',
+ gasPrice: '0x4',
+ recentBlocks: ['mockBlock'],
+ selectedAddress: '0x4',
+ selectedToken: { address: '0x1' },
+ to: 'mockTo',
+ value: 'mockValue',
+ }
+
+ it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
+ mapDispatchToPropsObject.updateAndSetGasTotal(mockProps)
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ actionSpies.setGasTotal.getCall(0).args[0],
+ '0x30x4'
+ )
+ })
+
+ it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
+ const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps
+ mapDispatchToPropsObject.updateAndSetGasTotal(
+ Object.assign({}, mockProps, {editingTransactionId: false})
+ )
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.updateGasData.getCall(0).args[0],
+ { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value }
+ )
+ })
+ })
+
+ describe('updateSendTokenBalance()', () => {
+ const mockProps = {
+ address: '0x10',
+ tokenContract: '0x00a',
+ selectedToken: {address: '0x1'},
+ }
+
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendTokenBalance(Object.assign({}, mockProps))
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.updateSendTokenBalance.getCall(0).args[0],
+ mockProps
+ )
+ })
+ })
+
+ describe('updateSendErrors()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendErrors('mockError')
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ 'mockError'
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send_/tests/send-selectors-test-data.js b/ui/app/components/send_/tests/send-selectors-test-data.js
new file mode 100644
index 000000000..8f9c19314
--- /dev/null
+++ b/ui/app/components/send_/tests/send-selectors-test-data.js
@@ -0,0 +1,230 @@
+module.exports = {
+ 'metamask': {
+ 'isInitialized': true,
+ 'isUnlocked': true,
+ 'featureFlags': {'betaUI': true},
+ 'rpcTarget': 'https://rawtestrpc.metamask.io/',
+ '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',
+ },
+ },
+ 'currentBlockGasLimit': '0x4c1878',
+ 'currentCurrency': 'USD',
+ 'conversionRate': 1200.88200327,
+ 'conversionDate': 1489013762,
+ 'noActiveNotices': true,
+ 'frequentRpcList': [],
+ 'network': '3',
+ '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': [
+ {
+ 'address': '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ 'name': 'Address Book Account 1',
+ },
+ ],
+ 'tokens': [
+ {
+ 'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896',
+ 'decimals': 18,
+ 'symbol': 'ABC',
+ },
+ {
+ 'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ 'decimals': 4,
+ 'symbol': 'DEF',
+ },
+ {
+ 'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b',
+ 'decimals': 18,
+ 'symbol': 'GHI',
+ },
+ ],
+ 'tokenExchangeRates': {
+ 'def_eth': {
+ rate: 2.0,
+ },
+ 'ghi_eth': {
+ rate: 31.01,
+ },
+ },
+ 'transactions': {},
+ 'selectedAddressTxList': [
+ {
+ 'id': 'mockTokenTx1',
+ 'txParams': {
+ 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ 'time': 1700000000000,
+ },
+ {
+ 'id': 'mockTokenTx2',
+ 'txParams': {
+ 'to': '0xafaketokenaddress',
+ },
+ 'time': 1600000000000,
+ },
+ {
+ 'id': 'mockTokenTx3',
+ 'txParams': {
+ 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ 'time': 1500000000000,
+ },
+ {
+ 'id': 'mockEthTx1',
+ 'txParams': {
+ 'to': '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ 'time': 1400000000000,
+ },
+ ],
+ 'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ 'unapprovedMsgs': {
+ '0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 },
+ '0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 },
+ '0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 },
+ },
+ 'unapprovedMsgCount': 0,
+ 'unapprovedPersonalMsgs': {},
+ 'unapprovedPersonalMsgCount': 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',
+ 'provider': {
+ 'type': 'testnet',
+ },
+ 'shapeShiftTxList': [
+ { id: 'shapeShiftTx1', 'time': 1675000000000 },
+ { id: 'shapeShiftTx2', 'time': 1575000000000 },
+ { id: 'shapeShiftTx3', 'time': 1475000000000 },
+ ],
+ 'lostAccounts': [],
+ 'send': {
+ 'gasLimit': '0xFFFF',
+ 'gasPrice': '0xaa',
+ 'gasTotal': '0xb451dc41b578',
+ 'tokenBalance': 3434,
+ 'from': {
+ 'address': '0xabcdefg',
+ 'balance': '0x5f4e3d2c1',
+ },
+ 'to': '0x987fedabc',
+ 'amount': '0x080',
+ 'memo': '',
+ 'errors': {
+ 'someError': null,
+ },
+ 'maxModeOn': false,
+ 'editingTransactionId': 97531,
+ 'forceGasMin': true,
+ },
+ 'unapprovedTxs': {
+ '4768706228115573': {
+ 'id': 4768706228115573,
+ 'time': 1487363153561,
+ 'status': 'unapproved',
+ 'gasMultiplier': 1,
+ 'metamaskNetworkId': '3',
+ 'txParams': {
+ 'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ 'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
+ 'value': '0xde0b6b3a7640000',
+ 'metamaskId': 4768706228115573,
+ 'metamaskNetworkId': '3',
+ 'gas': '0x5209',
+ },
+ 'gasLimitSpecified': false,
+ 'estimatedGas': '0x5209',
+ 'txFee': '17e0186e60800',
+ 'txValue': 'de0b6b3a7640000',
+ 'maxCost': 'de234b52e4a0800',
+ 'gasPrice': '4a817c800',
+ },
+ },
+ 'currentLocale': 'en',
+ recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'],
+ },
+ 'appState': {
+ 'menuOpen': false,
+ 'currentView': {
+ 'name': 'accountDetail',
+ 'detailView': null,
+ 'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ },
+ 'accountDetail': {
+ 'subview': 'transactions',
+ },
+ 'modal': {
+ 'modalState': {},
+ 'previousModalState': {},
+ },
+ 'transForward': true,
+ 'isLoading': false,
+ 'warning': null,
+ 'scrollToBottom': false,
+ 'forgottenPassword': null,
+ },
+ 'identities': {},
+ 'send': {
+ 'fromDropdownOpen': false,
+ 'toDropdownOpen': false,
+ 'errors': { 'someError': null },
+ },
+}
diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send_/tests/send-selectors.test.js
new file mode 100644
index 000000000..152af8059
--- /dev/null
+++ b/ui/app/components/send_/tests/send-selectors.test.js
@@ -0,0 +1,695 @@
+import assert from 'assert'
+import sinon from 'sinon'
+import selectors from '../send.selectors.js'
+const {
+ accountsWithSendEtherInfoSelector,
+ // autoAddToBetaUI,
+ getAddressBook,
+ getBlockGasLimit,
+ getAmountConversionRate,
+ getConversionRate,
+ getConvertedCurrency,
+ getCurrentAccountWithSendEtherInfo,
+ getCurrentCurrency,
+ getCurrentNetwork,
+ getCurrentViewContext,
+ getForceGasMin,
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getPrimaryCurrency,
+ getRecentBlocks,
+ getSelectedAccount,
+ getSelectedAddress,
+ getSelectedIdentity,
+ getSelectedToken,
+ getSelectedTokenContract,
+ getSelectedTokenExchangeRate,
+ getSelectedTokenToFiatRate,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendErrors,
+ getSendFrom,
+ getSendFromBalance,
+ getSendFromObject,
+ getSendMaxModeState,
+ getSendTo,
+ getSendToAccounts,
+ getTokenBalance,
+ getTokenExchangeRate,
+ getUnapprovedTxs,
+ transactionsSelector,
+} = selectors
+import mockState from './send-selectors-test-data'
+
+describe('send selectors', () => {
+ const tempGlobalEth = Object.assign({}, 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', () => {
+ assert.deepEqual(
+ accountsWithSendEtherInfoSelector(mockState),
+ [
+ {
+ 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('autoAddToBetaUI()', () => {
+ // it('should', () => {
+ // assert.deepEqual(
+ // autoAddToBetaUI(mockState),
+
+ // )
+ // })
+ // })
+
+ describe('getAddressBook()', () => {
+ it('should return the address book', () => {
+ assert.deepEqual(
+ getAddressBook(mockState),
+ [
+ {
+ address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ name: 'Address Book Account 1',
+ },
+ ],
+ )
+ })
+ })
+
+ describe('getAmountConversionRate()', () => {
+ it('should return the token conversion rate if a token is selected', () => {
+ assert.equal(
+ getAmountConversionRate(mockState),
+ 2401.76400654
+ )
+ })
+
+ it('should return the eth conversion rate if no token is selected', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, { selectedTokenAddress: null }),
+ }
+ assert.equal(
+ getAmountConversionRate(editedMockState),
+ 1200.88200327
+ )
+ })
+ })
+
+ describe('getBlockGasLimit', () => {
+ it('should return the current block gas limit', () => {
+ assert.deepEqual(
+ getBlockGasLimit(mockState),
+ '0x4c1878'
+ )
+ })
+ })
+
+ describe('getConversionRate()', () => {
+ it('should return the eth conversion rate', () => {
+ assert.deepEqual(
+ getConversionRate(mockState),
+ 1200.88200327
+ )
+ })
+ })
+
+ describe('getConvertedCurrency()', () => {
+ it('should return the currently selected currency', () => {
+ assert.equal(
+ getConvertedCurrency(mockState),
+ 'USD'
+ )
+ })
+ })
+
+ describe('getCurrentAccountWithSendEtherInfo()', () => {
+ it('should return the currently selected account with identity info', () => {
+ assert.deepEqual(
+ getCurrentAccountWithSendEtherInfo(mockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getCurrentCurrency()', () => {
+ it('should return the currently selected currency', () => {
+ assert.equal(
+ getCurrentCurrency(mockState),
+ 'USD'
+ )
+ })
+ })
+
+ describe('getCurrentNetwork()', () => {
+ it('should return the id of the currently selected network', () => {
+ assert.equal(
+ getCurrentNetwork(mockState),
+ '3'
+ )
+ })
+ })
+
+ describe('getCurrentViewContext()', () => {
+ it('should return the context of the current view', () => {
+ assert.equal(
+ getCurrentViewContext(mockState),
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
+ )
+ })
+ })
+
+ describe('getForceGasMin()', () => {
+ it('should get the send.forceGasMin property', () => {
+ assert.equal(
+ getForceGasMin(mockState),
+ true
+ )
+ })
+ })
+
+ describe('getGasLimit()', () => {
+ it('should return the send.gasLimit', () => {
+ assert.equal(
+ getGasLimit(mockState),
+ '0xFFFF'
+ )
+ })
+ })
+
+ describe('getGasPrice()', () => {
+ it('should return the send.gasPrice', () => {
+ assert.equal(
+ getGasPrice(mockState),
+ '0xaa'
+ )
+ })
+ })
+
+ describe('getGasTotal()', () => {
+ it('should return the send.gasTotal', () => {
+ assert.equal(
+ getGasTotal(mockState),
+ '0xb451dc41b578'
+ )
+ })
+ })
+
+ describe('getPrimaryCurrency()', () => {
+ it('should return the symbol of the selected token', () => {
+ assert.equal(
+ getPrimaryCurrency(mockState),
+ 'DEF'
+ )
+ })
+ })
+
+ describe('getRecentBlocks()', () => {
+ it('should return the recent blocks', () => {
+ assert.deepEqual(
+ getRecentBlocks(mockState),
+ ['mockBlock1', 'mockBlock2', 'mockBlock3']
+ )
+ })
+ })
+
+ describe('getSelectedAccount()', () => {
+ it('should return the currently selected account', () => {
+ assert.deepEqual(
+ getSelectedAccount(mockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ }
+ )
+ })
+ })
+
+ describe('getSelectedAddress()', () => {
+ it('should', () => {
+ assert.equal(
+ getSelectedAddress(mockState),
+ '0xd85a4b6a394794842887b8284293d69163007bbb'
+ )
+ })
+ })
+
+ describe('getSelectedIdentity()', () => {
+ it('should return the identity object of the currently selected address', () => {
+ assert.deepEqual(
+ getSelectedIdentity(mockState),
+ {
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getSelectedToken()', () => {
+ it('should return the currently selected token if selected', () => {
+ assert.deepEqual(
+ getSelectedToken(mockState),
+ {
+ address: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ decimals: 4,
+ symbol: 'DEF',
+ }
+ )
+ })
+
+ it('should return the send token if none is currently selected, but a send token exists', () => {
+ const mockSendToken = {
+ address: '0x123456708414189a58339873ab429b6c47ab92d3',
+ decimals: 4,
+ symbol: 'JKL',
+ }
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ selectedTokenAddress: null,
+ send: {
+ token: mockSendToken,
+ },
+ }),
+ }
+ assert.deepEqual(
+ getSelectedToken(editedMockState),
+ Object.assign({}, mockSendToken)
+ )
+ })
+ })
+
+ describe('getSelectedTokenContract()', () => {
+ it('should return the contract at the selected token address', () => {
+ assert.equal(
+ getSelectedTokenContract(mockState),
+ 'mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3'
+ )
+ })
+
+ it('should return null if no token is selected', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
+ assert.equal(
+ getSelectedTokenContract(Object.assign({}, mockState, { metamask: modifiedMetamaskState })),
+ null
+ )
+ })
+ })
+
+ describe('getSelectedTokenExchangeRate()', () => {
+ it('should return the exchange rate for the selected token', () => {
+ assert.equal(
+ getSelectedTokenExchangeRate(mockState),
+ 2.0
+ )
+ })
+ })
+
+ describe('getSelectedTokenToFiatRate()', () => {
+ it('should return rate for converting the selected token to fiat', () => {
+ assert.equal(
+ getSelectedTokenToFiatRate(mockState),
+ 2401.76400654
+ )
+ })
+ })
+
+ describe('getSendAmount()', () => {
+ it('should return the send.amount', () => {
+ assert.equal(
+ getSendAmount(mockState),
+ '0x080'
+ )
+ })
+ })
+
+ describe('getSendEditingTransactionId()', () => {
+ it('should return the send.editingTransactionId', () => {
+ assert.equal(
+ getSendEditingTransactionId(mockState),
+ 97531
+ )
+ })
+ })
+
+ describe('getSendErrors()', () => {
+ it('should return the send.errors', () => {
+ assert.deepEqual(
+ getSendErrors(mockState),
+ { someError: null }
+ )
+ })
+ })
+
+ describe('getSendFrom()', () => {
+ it('should return the send.from', () => {
+ assert.deepEqual(
+ getSendFrom(mockState),
+ {
+ address: '0xabcdefg',
+ balance: '0x5f4e3d2c1',
+ }
+ )
+ })
+ })
+
+ describe('getSendFromBalance()', () => {
+ it('should get the send.from balance if it exists', () => {
+ assert.equal(
+ getSendFromBalance(mockState),
+ '0x5f4e3d2c1'
+ )
+ })
+
+ it('should get the selected account balance if the send.from does not exist', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ send: {
+ from: null,
+ },
+ }),
+ }
+ assert.equal(
+ getSendFromBalance(editedMockState),
+ '0x0'
+ )
+ })
+ })
+
+ describe('getSendFromObject()', () => {
+ it('should return send.from if it exists', () => {
+ assert.deepEqual(
+ getSendFromObject(mockState),
+ {
+ address: '0xabcdefg',
+ balance: '0x5f4e3d2c1',
+ }
+ )
+ })
+
+ it('should return the current account with send ether info if send.from does not exist', () => {
+ const editedMockState = {
+ metamask: Object.assign({}, mockState.metamask, {
+ send: {
+ from: null,
+ },
+ }),
+ }
+ assert.deepEqual(
+ getSendFromObject(editedMockState),
+ {
+ code: '0x',
+ balance: '0x0',
+ nonce: '0x0',
+ address: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ name: 'Send Account 4',
+ }
+ )
+ })
+ })
+
+ describe('getSendMaxModeState()', () => {
+ it('should return send.maxModeOn', () => {
+ assert.equal(
+ getSendMaxModeState(mockState),
+ false
+ )
+ })
+ })
+
+ describe('getSendTo()', () => {
+ it('should return send.to', () => {
+ assert.equal(
+ getSendTo(mockState),
+ '0x987fedabc'
+ )
+ })
+ })
+
+ describe('getSendToAccounts()', () => {
+ it('should return an array including all the users accounts and the address book', () => {
+ assert.deepEqual(
+ getSendToAccounts(mockState),
+ [
+ {
+ 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',
+ },
+ {
+ address: '0x06195827297c7a80a443b6894d3bdb8824b43896',
+ name: 'Address Book Account 1',
+ },
+ ]
+ )
+ })
+ })
+
+ describe('getTokenBalance()', () => {
+ it('should', () => {
+ assert.equal(
+ getTokenBalance(mockState),
+ 3434
+ )
+ })
+ })
+
+ describe('getTokenExchangeRate()', () => {
+ it('should return the passed tokens exchange rates', () => {
+ assert.equal(
+ getTokenExchangeRate(mockState, 'GHI'),
+ 31.01
+ )
+ })
+ })
+
+ describe('getUnapprovedTxs()', () => {
+ it('should return the unapproved txs', () => {
+ assert.deepEqual(
+ getUnapprovedTxs(mockState),
+ {
+ 4768706228115573: {
+ id: 4768706228115573,
+ time: 1487363153561,
+ status: 'unapproved',
+ gasMultiplier: 1,
+ metamaskNetworkId: '3',
+ txParams: {
+ from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
+ to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761',
+ value: '0xde0b6b3a7640000',
+ metamaskId: 4768706228115573,
+ metamaskNetworkId: '3',
+ gas: '0x5209',
+ },
+ gasLimitSpecified: false,
+ estimatedGas: '0x5209',
+ txFee: '17e0186e60800',
+ txValue: 'de0b6b3a7640000',
+ maxCost: 'de234b52e4a0800',
+ gasPrice: '4a817c800',
+ },
+ }
+ )
+ })
+ })
+
+ describe('transactionsSelector()', () => {
+ it('should return the selected addresses selected token transactions', () => {
+ assert.deepEqual(
+ transactionsSelector(mockState),
+ [
+ {
+ id: 'mockTokenTx1',
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ time: 1700000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ time: 1500000000000,
+ },
+ ]
+ )
+ })
+
+ it('should return all transactions if no token is selected', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false })
+ const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
+ assert.deepEqual(
+ transactionsSelector(modifiedState),
+ [
+ {
+ id: 'mockTokenTx1',
+ time: 1700000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ {
+ id: 'unapprovedMessage1',
+ time: 1650000000000,
+ },
+ {
+ id: 'mockTokenTx2',
+ time: 1600000000000,
+ txParams: {
+ to: '0xafaketokenaddress',
+ },
+ },
+ {
+ id: 'unapprovedMessage2',
+ time: 1550000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ time: 1500000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ {
+ id: 'unapprovedMessage3',
+ time: 1450000000000,
+ },
+ {
+ id: 'mockEthTx1',
+ time: 1400000000000,
+ txParams: {
+ to: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ },
+ ]
+ )
+ })
+
+ it('should return shapeshift transactions if current network is 1', () => {
+ const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false, network: '1' })
+ const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState })
+ assert.deepEqual(
+ transactionsSelector(modifiedState),
+ [
+ {
+ id: 'mockTokenTx1',
+ time: 1700000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ { id: 'shapeShiftTx1', 'time': 1675000000000 },
+ {
+ id: 'unapprovedMessage1',
+ time: 1650000000000,
+ },
+ {
+ id: 'mockTokenTx2',
+ time: 1600000000000,
+ txParams: {
+ to: '0xafaketokenaddress',
+ },
+ },
+ { id: 'shapeShiftTx2', 'time': 1575000000000 },
+ {
+ id: 'unapprovedMessage2',
+ time: 1550000000000,
+ },
+ {
+ id: 'mockTokenTx3',
+ time: 1500000000000,
+ txParams: {
+ to: '0x8d6b81208414189a58339873ab429b6c47ab92d3',
+ },
+ },
+ { id: 'shapeShiftTx3', 'time': 1475000000000 },
+ {
+ id: 'unapprovedMessage3',
+ time: 1450000000000,
+ },
+ {
+ id: 'mockEthTx1',
+ time: 1400000000000,
+ txParams: {
+ to: '0xd85a4b6a394794842887b8284293d69163007bbb',
+ },
+ },
+ ]
+ )
+ })
+ })
+
+})
diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js
new file mode 100644
index 000000000..b3f6372ef
--- /dev/null
+++ b/ui/app/components/send_/tests/send-utils.test.js
@@ -0,0 +1,404 @@
+import assert from 'assert'
+import sinon from 'sinon'
+import proxyquire from 'proxyquire'
+import {
+ ONE_GWEI_IN_WEI_HEX,
+ SIMPLE_GAS_COST,
+} from '../send.constants'
+const {
+ addCurrencies,
+ subtractCurrencies,
+} = require('../../../conversion-util')
+
+const {
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+} = require('../send.constants')
+
+const stubs = {
+ addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
+ conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
+ conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
+ multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
+ calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
+ rawEncode: sinon.stub().returns([16, 1100]),
+}
+
+const sendUtils = proxyquire('../send.utils.js', {
+ '../../conversion-util': {
+ addCurrencies: stubs.addCurrencies,
+ conversionUtil: stubs.conversionUtil,
+ conversionGTE: stubs.conversionGTE,
+ multiplyCurrencies: stubs.multiplyCurrencies,
+ },
+ '../../token-util': { calcTokenAmount: stubs.calcTokenAmount },
+ 'ethereumjs-abi': {
+ rawEncode: stubs.rawEncode,
+ },
+})
+
+const {
+ calcGasTotal,
+ estimateGas,
+ doesAmountErrorRequireUpdate,
+ estimateGasPriceFromRecentBlocks,
+ generateTokenTransferData,
+ getAmountErrorObject,
+ calcTokenBalance,
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+} = sendUtils
+
+describe('send utils', () => {
+
+ describe('calcGasTotal()', () => {
+ it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => {
+ const result = calcGasTotal(12, 15)
+ assert.equal(result, '12x15')
+ const call_ = stubs.multiplyCurrencies.getCall(0).args
+ assert.deepEqual(
+ call_,
+ [12, 15, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ } ]
+ )
+ })
+ })
+
+ 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,
+ selectedToken: 'someToken',
+ expectedResult: true,
+ },
+ 'should return false if they are all the same': {
+ balance: 1,
+ prevBalance: 1,
+ gasTotal: 1,
+ prevGasTotal: 1,
+ tokenBalance: 1,
+ prevTokenBalance: 1,
+ selectedToken: 'someToken',
+ expectedResult: false,
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult)
+ })
+ })
+
+ })
+
+ describe('generateTokenTransferData()', () => {
+ it('should return undefined if not passed a selected token', () => {
+ assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined)
+ })
+
+ it('should call abi.rawEncode with the correct params', () => {
+ stubs.rawEncode.resetHistory()
+ generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true})
+ assert.deepEqual(
+ stubs.rawEncode.getCall(0).args,
+ [['address', 'uint256'], ['mockAddress', '0xab']]
+ )
+ })
+
+ it('should return encoded token transfer data', () => {
+ assert.equal(
+ generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}),
+ '0xa9059cbb104c'
+ )
+ })
+ })
+
+ describe('getAmountErrorObject()', () => {
+ const config = {
+ 'should return insufficientFunds error if isBalanceSufficient returns false': {
+ amount: 15,
+ amountConversionRate: 2,
+ balance: 1,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
+ },
+ 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
+ amount: '0x10',
+ amountConversionRate: 2,
+ balance: 100,
+ conversionRate: 3,
+ decimals: 10,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ selectedToken: 'someToken',
+ tokenBalance: 123,
+ expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR },
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult)
+ })
+ })
+ })
+
+ describe('calcTokenBalance()', () => {
+ it('should return the calculated token blance', () => {
+ assert.equal(calcTokenBalance({
+ selectedToken: {
+ decimals: 11,
+ },
+ usersToken: {
+ balance: 20,
+ },
+ }), 'calc:2011')
+ })
+ })
+
+ describe('isBalanceSufficient()', () => {
+ it('should correctly call addCurrencies and return the result of calling conversionGTE', () => {
+ stubs.conversionGTE.resetHistory()
+ const result = isBalanceSufficient({
+ amount: 15,
+ amountConversionRate: 2,
+ balance: 100,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ })
+ assert.deepEqual(
+ stubs.addCurrencies.getCall(0).args,
+ [
+ 15, 17, {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ },
+ ]
+ )
+ assert.deepEqual(
+ stubs.conversionGTE.getCall(0).args,
+ [
+ {
+ value: 100,
+ fromNumericBase: 'hex',
+ fromCurrency: 'ABC',
+ conversionRate: 3,
+ },
+ {
+ value: 32,
+ fromNumericBase: 'hex',
+ conversionRate: 2,
+ fromCurrency: 'ABC',
+ },
+ ]
+ )
+
+ assert.equal(result, true)
+ })
+ })
+
+ describe('isTokenBalanceSufficient()', () => {
+ it('should correctly call conversionUtil and return the result of calling conversionGTE', () => {
+ stubs.conversionGTE.resetHistory()
+ const result = isTokenBalanceSufficient({
+ amount: '0x10',
+ tokenBalance: 123,
+ decimals: 10,
+ })
+ assert.deepEqual(
+ stubs.conversionUtil.getCall(0).args,
+ [
+ '0x10', {
+ fromNumericBase: 'hex',
+ },
+ ]
+ )
+ assert.deepEqual(
+ stubs.conversionGTE.getCall(0).args,
+ [
+ {
+ value: 123,
+ fromNumericBase: 'dec',
+ },
+ {
+ value: 'calc:1610',
+ fromNumericBase: 'dec',
+ },
+ ]
+ )
+
+ assert.equal(result, false)
+ })
+ })
+
+ describe('estimateGas', () => {
+ const baseMockParams = {
+ blockGasLimit: '0x64',
+ selectedAddress: 'mockAddress',
+ to: '0xisContract',
+ estimateGasMethod: sinon.stub().callsFake(
+ (data, cb) => cb(
+ data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null,
+ { toString: (n) => `mockToString:${n}` }
+ )
+ ),
+ }
+ const baseExpectedCall = {
+ from: 'mockAddress',
+ gas: '0x64x0.95',
+ to: '0xisContract',
+ }
+
+ beforeEach(() => {
+ global.eth = {
+ getCode: sinon.stub().callsFake(
+ (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
+ ),
+ }
+ })
+
+ afterEach(() => {
+ baseMockParams.estimateGasMethod.resetHistory()
+ global.eth.getCode.resetHistory()
+ })
+
+ it('should call ethQuery.estimateGas with the expected params', async () => {
+ const result = await estimateGas(baseMockParams)
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
+ )
+ assert.equal(result, 'mockToString:16')
+ })
+
+ it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
+ const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams))
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
+ assert.deepEqual(
+ baseMockParams.estimateGasMethod.getCall(0).args[0],
+ Object.assign({}, baseExpectedCall, {
+ gasPrice: undefined,
+ value: '0x0',
+ data: '0xa9059cbb104c',
+ to: 'mockAddress',
+ })
+ )
+ assert.equal(result, 'mockToString:16')
+ })
+
+ it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' }))
+ assert.equal(result, SIMPLE_GAS_COST)
+ })
+
+ it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => {
+ assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
+ const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
+ assert.notEqual(result, SIMPLE_GAS_COST)
+ })
+
+ it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
+ const result = await estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:Transaction execution error.',
+ }))
+ assert.equal(result, '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 estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.',
+ }))
+ assert.equal(result, '0x64x0.95')
+ })
+
+ it(`should reject other errors`, async () => {
+ try {
+ await estimateGas(Object.assign({}, baseMockParams, {
+ to: 'isContract willFailBecauseOf:some other error',
+ }))
+ } catch (err) {
+ assert.deepEqual(err, { message: 'some other error' })
+ }
+ })
+ })
+
+ describe('estimateGasPriceFromRecentBlocks', () => {
+ const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+ const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+ const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+
+ it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => {
+ assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => {
+ assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: null },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
+ })
+
+ it(`should return the middle value of all blocks lowest prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
+ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE)
+ })
+
+ it(`should work if a block has multiple gas prices`, () => {
+ const mockRecentBlocks = [
+ { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] },
+ { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] },
+ { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] },
+ ]
+ assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
+ })
+ })
+})