aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/send_/send-footer
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components/send_/send-footer')
-rw-r--r--ui/app/components/send_/send-footer/README.md0
-rw-r--r--ui/app/components/send_/send-footer/index.js1
-rw-r--r--ui/app/components/send_/send-footer/send-footer.component.js99
-rw-r--r--ui/app/components/send_/send-footer/send-footer.container.js100
-rw-r--r--ui/app/components/send_/send-footer/send-footer.scss0
-rw-r--r--ui/app/components/send_/send-footer/send-footer.selectors.js11
-rw-r--r--ui/app/components/send_/send-footer/send-footer.utils.js81
-rw-r--r--ui/app/components/send_/send-footer/tests/send-footer-component.test.js227
-rw-r--r--ui/app/components/send_/send-footer/tests/send-footer-container.test.js191
-rw-r--r--ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js24
-rw-r--r--ui/app/components/send_/send-footer/tests/send-footer-utils.test.js210
11 files changed, 944 insertions, 0 deletions
diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send_/send-footer/README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/components/send_/send-footer/README.md
diff --git a/ui/app/components/send_/send-footer/index.js b/ui/app/components/send_/send-footer/index.js
new file mode 100644
index 000000000..cd1727330
--- /dev/null
+++ b/ui/app/components/send_/send-footer/index.js
@@ -0,0 +1 @@
+export { default } from './send-footer.container' \ No newline at end of file
diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js
new file mode 100644
index 000000000..6471ae1a3
--- /dev/null
+++ b/ui/app/components/send_/send-footer/send-footer.component.js
@@ -0,0 +1,99 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../page-container/page-container-footer'
+import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes'
+
+export default class SendFooter extends Component {
+
+ static propTypes = {
+ addToAddressBookIfNew: PropTypes.func,
+ amount: PropTypes.string,
+ clearSend: PropTypes.func,
+ disabled: PropTypes.bool,
+ editingTransactionId: PropTypes.string,
+ errors: PropTypes.object,
+ from: PropTypes.object,
+ gasLimit: PropTypes.string,
+ gasPrice: PropTypes.string,
+ gasTotal: PropTypes.string,
+ history: PropTypes.object,
+ inError: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ sign: PropTypes.func,
+ to: PropTypes.string,
+ toAccounts: PropTypes.array,
+ tokenBalance: PropTypes.string,
+ unapprovedTxs: PropTypes.object,
+ update: PropTypes.func,
+ };
+
+ onCancel () {
+ this.props.clearSend()
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ onSubmit (event) {
+ event.preventDefault()
+ const {
+ addToAddressBookIfNew,
+ amount,
+ editingTransactionId,
+ from: {address: from},
+ gasLimit: gas,
+ gasPrice,
+ selectedToken,
+ sign,
+ to,
+ unapprovedTxs,
+ // updateTx,
+ update,
+ toAccounts,
+ } = this.props
+
+ // 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
+ addToAddressBookIfNew(to, toAccounts)
+
+ editingTransactionId
+ ? update({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+ : sign({ selectedToken, to, amount, from, gas, gasPrice })
+
+ this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+
+ formShouldBeDisabled () {
+ const { inError, selectedToken, tokenBalance, gasTotal, to } = this.props
+ const missingTokenBalance = selectedToken && !tokenBalance
+ return inError || !gasTotal || missingTokenBalance || !to
+ }
+
+ render () {
+ return (
+ <PageContainerFooter
+ onCancel={() => this.onCancel()}
+ onSubmit={e => this.onSubmit(e)}
+ disabled={this.formShouldBeDisabled()}
+ />
+ )
+ }
+
+}
+
+SendFooter.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send_/send-footer/send-footer.container.js
new file mode 100644
index 000000000..260ff40bc
--- /dev/null
+++ b/ui/app/components/send_/send-footer/send-footer.container.js
@@ -0,0 +1,100 @@
+import { connect } from 'react-redux'
+import ethUtil from 'ethereumjs-util'
+import {
+ addToAddressBook,
+ clearSend,
+ signTokenTx,
+ signTx,
+ updateTransaction,
+} from '../../../actions'
+import SendFooter from './send-footer.component'
+import {
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getSelectedToken,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendFromObject,
+ getSendTo,
+ getSendToAccounts,
+ getTokenBalance,
+ getUnapprovedTxs,
+} from '../send.selectors'
+import {
+ isSendFormInError,
+} from './send-footer.selectors'
+import {
+ addressIsNew,
+ constructTxParams,
+ constructUpdatedTx,
+} from './send-footer.utils'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
+
+function mapStateToProps (state) {
+ return {
+ amount: getSendAmount(state),
+ editingTransactionId: getSendEditingTransactionId(state),
+ from: getSendFromObject(state),
+ gasLimit: getGasLimit(state),
+ gasPrice: getGasPrice(state),
+ gasTotal: getGasTotal(state),
+ inError: isSendFormInError(state),
+ selectedToken: getSelectedToken(state),
+ to: getSendTo(state),
+ toAccounts: getSendToAccounts(state),
+ tokenBalance: getTokenBalance(state),
+ unapprovedTxs: getUnapprovedTxs(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ clearSend: () => dispatch(clearSend()),
+ sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => {
+ const txParams = constructTxParams({
+ amount,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ })
+
+ selectedToken
+ ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams))
+ : dispatch(signTx(txParams))
+ },
+ update: ({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ }) => {
+ const editingTx = constructUpdatedTx({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+
+ dispatch(updateTransaction(editingTx))
+ },
+ addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
+ const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
+ if (addressIsNew(toAccounts)) {
+ // TODO: nickname, i.e. addToAddressBook(recipient, nickname)
+ dispatch(addToAddressBook(hexPrefixedAddress, nickname))
+ }
+ },
+ }
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send_/send-footer/send-footer.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/components/send_/send-footer/send-footer.scss
diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send_/send-footer/send-footer.selectors.js
new file mode 100644
index 000000000..e20addfdc
--- /dev/null
+++ b/ui/app/components/send_/send-footer/send-footer.selectors.js
@@ -0,0 +1,11 @@
+const { getSendErrors } = require('../send.selectors')
+
+const selectors = {
+ isSendFormInError,
+}
+
+module.exports = selectors
+
+function isSendFormInError (state) {
+ return Object.values(getSendErrors(state)).some(n => n)
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send_/send-footer/send-footer.utils.js
new file mode 100644
index 000000000..875e7d948
--- /dev/null
+++ b/ui/app/components/send_/send-footer/send-footer.utils.js
@@ -0,0 +1,81 @@
+const ethAbi = require('ethereumjs-abi')
+const ethUtil = require('ethereumjs-util')
+const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../send.constants')
+
+function addHexPrefixToObjectValues (obj) {
+ return Object.keys(obj).reduce((newObj, key) => {
+ return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) }
+ }, {})
+}
+
+function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) {
+ const txParams = {
+ from,
+ value: '0',
+ gas,
+ gasPrice,
+ }
+
+ if (!selectedToken) {
+ txParams.value = amount
+ txParams.to = to
+ }
+
+ const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams)
+
+ return hexPrefixedTxParams
+}
+
+function constructUpdatedTx ({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+}) {
+ const editingTx = {
+ ...unapprovedTxs[editingTransactionId],
+ txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }),
+ }
+
+ if (selectedToken) {
+ const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
+ ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
+ value: '0',
+ to: selectedToken.address,
+ data,
+ }))
+ } else {
+ const { data } = unapprovedTxs[editingTransactionId].txParams
+
+ Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
+ value: amount,
+ to,
+ data,
+ }))
+
+ if (typeof editingTx.txParams.data === 'undefined') {
+ delete editingTx.txParams.data
+ }
+ }
+
+ return editingTx
+}
+
+function addressIsNew (toAccounts, newAddress) {
+ return !toAccounts.find(({ address }) => newAddress === address)
+}
+
+module.exports = {
+ addressIsNew,
+ constructTxParams,
+ constructUpdatedTx,
+ addHexPrefixToObjectValues,
+}
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js
new file mode 100644
index 000000000..e071fe54f
--- /dev/null
+++ b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js
@@ -0,0 +1,227 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../routes'
+import SendFooter from '../send-footer.component.js'
+
+import PageContainerFooter from '../../../page-container/page-container-footer'
+
+const propsMethodSpies = {
+ addToAddressBookIfNew: sinon.spy(),
+ clearSend: sinon.spy(),
+ sign: sinon.spy(),
+ update: sinon.spy(),
+}
+const historySpies = {
+ push: sinon.spy(),
+}
+const MOCK_EVENT = { preventDefault: () => {} }
+
+sinon.spy(SendFooter.prototype, 'onCancel')
+sinon.spy(SendFooter.prototype, 'onSubmit')
+
+describe('SendFooter Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendFooter
+ addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
+ amount={'mockAmount'}
+ clearSend={propsMethodSpies.clearSend}
+ disabled={true}
+ editingTransactionId={'mockEditingTransactionId'}
+ errors={{}}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={historySpies}
+ inError={false}
+ selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
+ sign={propsMethodSpies.sign}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ tokenBalance={'mockTokenBalance'}
+ unapprovedTxs={['mockTx']}
+ update={propsMethodSpies.update}
+ />, { context: { t: str => str } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.clearSend.resetHistory()
+ propsMethodSpies.addToAddressBookIfNew.resetHistory()
+ propsMethodSpies.clearSend.resetHistory()
+ propsMethodSpies.sign.resetHistory()
+ propsMethodSpies.update.resetHistory()
+ historySpies.push.resetHistory()
+ SendFooter.prototype.onCancel.resetHistory()
+ SendFooter.prototype.onSubmit.resetHistory()
+ })
+
+ describe('onCancel', () => {
+ it('should call clearSend', () => {
+ assert.equal(propsMethodSpies.clearSend.callCount, 0)
+ wrapper.instance().onCancel()
+ assert.equal(propsMethodSpies.clearSend.callCount, 1)
+ })
+
+ it('should call history.push', () => {
+ assert.equal(historySpies.push.callCount, 0)
+ wrapper.instance().onCancel()
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE)
+ })
+ })
+
+
+ describe('formShouldBeDisabled()', () => {
+ const config = {
+ 'should return true if inError is truthy': {
+ inError: true,
+ expectedResult: true,
+ },
+ 'should return true if gasTotal is falsy': {
+ inError: false,
+ gasTotal: false,
+ expectedResult: true,
+ },
+ 'should return true if to is truthy': {
+ to: '0xsomevalidAddress',
+ inError: false,
+ gasTotal: false,
+ expectedResult: true,
+ },
+ 'should return true if selectedToken is truthy and tokenBalance is falsy': {
+ selectedToken: true,
+ tokenBalance: null,
+ expectedResult: true,
+ },
+ 'should return false if inError is false and all other params are truthy': {
+ inError: false,
+ gasTotal: '0x123',
+ selectedToken: true,
+ tokenBalance: 123,
+ expectedResult: false,
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ wrapper.setProps(obj)
+ assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult)
+ })
+ })
+ })
+
+ describe('onSubmit', () => {
+ it('should call addToAddressBookIfNew with the correct params', () => {
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.addToAddressBookIfNew.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.addToAddressBookIfNew.getCall(0).args,
+ ['mockTo', ['mockAccount']]
+ )
+ })
+
+ it('should call props.update if editingTransactionId is truthy', () => {
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.update.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.update.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ editingTransactionId: 'mockEditingTransactionId',
+ from: 'mockAddress',
+ gas: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ selectedToken: { mockProp: 'mockSelectedTokenProp' },
+ to: 'mockTo',
+ unapprovedTxs: ['mockTx'],
+ }
+ )
+ })
+
+ it('should not call props.sign if editingTransactionId is truthy', () => {
+ assert.equal(propsMethodSpies.sign.callCount, 0)
+ })
+
+ it('should call props.sign if editingTransactionId is falsy', () => {
+ wrapper.setProps({ editingTransactionId: null })
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert(propsMethodSpies.sign.calledOnce)
+ assert.deepEqual(
+ propsMethodSpies.sign.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ from: 'mockAddress',
+ gas: 'mockGasLimit',
+ gasPrice: 'mockGasPrice',
+ selectedToken: { mockProp: 'mockSelectedTokenProp' },
+ to: 'mockTo',
+ }
+ )
+ })
+
+ it('should not call props.update if editingTransactionId is falsy', () => {
+ assert.equal(propsMethodSpies.update.callCount, 0)
+ })
+
+ it('should call history.push', () => {
+ wrapper.instance().onSubmit(MOCK_EVENT)
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ })
+ })
+
+ describe('render', () => {
+ beforeEach(() => {
+ sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn')
+ wrapper = shallow(<SendFooter
+ addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew}
+ amount={'mockAmount'}
+ clearSend={propsMethodSpies.clearSend}
+ disabled={true}
+ editingTransactionId={'mockEditingTransactionId'}
+ errors={{}}
+ from={ { address: 'mockAddress', balance: 'mockBalance' } }
+ gasLimit={'mockGasLimit'}
+ gasPrice={'mockGasPrice'}
+ gasTotal={'mockGasTotal'}
+ history={historySpies}
+ inError={false}
+ selectedToken={{ mockProp: 'mockSelectedTokenProp' }}
+ sign={propsMethodSpies.sign}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ tokenBalance={'mockTokenBalance'}
+ unapprovedTxs={['mockTx']}
+ update={propsMethodSpies.update}
+ />, { context: { t: str => str } })
+ })
+
+ afterEach(() => {
+ SendFooter.prototype.formShouldBeDisabled.restore()
+ })
+
+ it('should render a PageContainerFooter component', () => {
+ assert.equal(wrapper.find(PageContainerFooter).length, 1)
+ })
+
+ it('should pass the correct props to PageContainerFooter', () => {
+ const {
+ onCancel,
+ onSubmit,
+ disabled,
+ } = wrapper.find(PageContainerFooter).props()
+ assert.equal(disabled, 'formShouldBeDisabledReturn')
+
+ assert.equal(SendFooter.prototype.onSubmit.callCount, 0)
+ onSubmit(MOCK_EVENT)
+ assert.equal(SendFooter.prototype.onSubmit.callCount, 1)
+
+ assert.equal(SendFooter.prototype.onCancel.callCount, 0)
+ onCancel()
+ assert.equal(SendFooter.prototype.onCancel.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js b/ui/app/components/send_/send-footer/tests/send-footer-container.test.js
new file mode 100644
index 000000000..39d6a7686
--- /dev/null
+++ b/ui/app/components/send_/send-footer/tests/send-footer-container.test.js
@@ -0,0 +1,191 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+
+const actionSpies = {
+ addToAddressBook: sinon.spy(),
+ clearSend: sinon.spy(),
+ signTokenTx: sinon.spy(),
+ signTx: sinon.spy(),
+ updateTransaction: sinon.spy(),
+}
+const utilsStubs = {
+ addressIsNew: sinon.stub().returns(true),
+ constructTxParams: sinon.stub().returns('mockConstructedTxParams'),
+ constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'),
+}
+
+proxyquire('../send-footer.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ return () => ({})
+ },
+ },
+ '../../../actions': actionSpies,
+ '../send.selectors': {
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendAmount: (s) => `mockAmount:${s}`,
+ getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
+ getSendFromObject: (s) => `mockFromObject:${s}`,
+ getSendTo: (s) => `mockTo:${s}`,
+ getSendToAccounts: (s) => `mockToAccounts:${s}`,
+ getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
+ },
+ './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
+ './send-footer.utils': utilsStubs,
+})
+
+describe('send-footer container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ amount: 'mockAmount:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ editingTransactionId: 'mockEditingTransactionId:mockState',
+ from: 'mockFromObject:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ gasTotal: 'mockGasTotal:mockState',
+ inError: 'mockInError:mockState',
+ to: 'mockTo:mockState',
+ toAccounts: 'mockToAccounts:mockState',
+ tokenBalance: 'mockTokenBalance:mockState',
+ unapprovedTxs: 'mockUnapprovedTxs:mockState',
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ })
+
+ describe('clearSend()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.clearSend()
+ assert(dispatchSpy.calledOnce)
+ assert(actionSpies.clearSend.calledOnce)
+ })
+ })
+
+ describe('sign()', () => {
+ it('should dispatch a signTokenTx action if selectedToken is defined', () => {
+ mapDispatchToPropsObject.sign({
+ selectedToken: {
+ address: '0xabc',
+ },
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructTxParams.getCall(0).args[0],
+ {
+ selectedToken: {
+ address: '0xabc',
+ },
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }
+ )
+ assert.deepEqual(
+ actionSpies.signTokenTx.getCall(0).args,
+ [ '0xabc', 'mockTo', 'mockAmount', 'mockConstructedTxParams' ]
+ )
+ })
+
+ it('should dispatch a sign action if selectedToken is not defined', () => {
+ utilsStubs.constructTxParams.resetHistory()
+ mapDispatchToPropsObject.sign({
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructTxParams.getCall(0).args[0],
+ {
+ selectedToken: undefined,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }
+ )
+ assert.deepEqual(
+ actionSpies.signTx.getCall(0).args,
+ [ 'mockConstructedTxParams' ]
+ )
+ })
+ })
+
+ describe('update()', () => {
+ it('should dispatch an updateTransaction action', () => {
+ mapDispatchToPropsObject.update({
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ editingTransactionId: 'mockEditingTransactionId',
+ selectedToken: 'mockSelectedToken',
+ unapprovedTxs: 'mockUnapprovedTxs',
+ })
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ utilsStubs.constructUpdatedTx.getCall(0).args[0],
+ {
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ editingTransactionId: 'mockEditingTransactionId',
+ selectedToken: 'mockSelectedToken',
+ unapprovedTxs: 'mockUnapprovedTxs',
+ }
+ )
+ assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams')
+ })
+ })
+
+ describe('addToAddressBookIfNew()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname')
+ assert(dispatchSpy.calledOnce)
+ assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts')
+ assert.deepEqual(
+ actionSpies.addToAddressBook.getCall(0).args,
+ [ '0xmockNewAddress', 'mockNickname' ]
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js
new file mode 100644
index 000000000..8de032f57
--- /dev/null
+++ b/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js
@@ -0,0 +1,24 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+const {
+ isSendFormInError,
+} = proxyquire('../send-footer.selectors', {
+ '../send.selectors': {
+ getSendErrors: (mockState) => mockState.errors,
+ },
+})
+
+describe('send-footer selectors', () => {
+
+ describe('getTitleKey()', () => {
+ it('should return true if any of the values of the object returned by getSendErrors are truthy', () => {
+ assert.equal(isSendFormInError({ errors: { a: 'abc', b: false} }), true)
+ })
+
+ it('should return false if all of the values of the object returned by getSendErrors are falsy', () => {
+ assert.equal(isSendFormInError({ errors: { a: false, b: null} }), false)
+ })
+ })
+
+})
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js
new file mode 100644
index 000000000..2d3135995
--- /dev/null
+++ b/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js
@@ -0,0 +1,210 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../../send.constants')
+
+const stubs = {
+ rawEncode: sinon.stub().callsFake((arr1, arr2) => {
+ return [ ...arr1, ...arr2 ]
+ }),
+}
+
+const sendUtils = proxyquire('../send-footer.utils.js', {
+ 'ethereumjs-abi': {
+ rawEncode: stubs.rawEncode,
+ },
+})
+const {
+ addressIsNew,
+ constructTxParams,
+ constructUpdatedTx,
+ addHexPrefixToObjectValues,
+} = sendUtils
+
+describe('send-footer utils', () => {
+
+ describe('addHexPrefixToObjectValues()', () => {
+ it('should return a new object with the same properties with a 0x prefix', () => {
+ assert.deepEqual(
+ addHexPrefixToObjectValues({
+ prop1: '0x123',
+ prop2: '456',
+ prop3: 'x',
+ }),
+ {
+ prop1: '0x123',
+ prop2: '0x456',
+ prop3: '0xx',
+ }
+ )
+ })
+ })
+
+ describe('addressIsNew()', () => {
+ it('should return false if the address exists in toAccounts', () => {
+ assert.equal(
+ addressIsNew([
+ { address: '0xabc' },
+ { address: '0xdef' },
+ { address: '0xghi' },
+ ], '0xdef'),
+ false
+ )
+ })
+
+ it('should return true if the address does not exists in toAccounts', () => {
+ assert.equal(
+ addressIsNew([
+ { address: '0xabc' },
+ { address: '0xdef' },
+ { address: '0xghi' },
+ ], '0xxyz'),
+ true
+ )
+ })
+ })
+
+ describe('constructTxParams()', () => {
+ it('should return a new txParams object with value and to properties if there is no selectedToken', () => {
+ assert.deepEqual(
+ constructTxParams({
+ selectedToken: false,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }),
+ {
+ 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 selectedToken', () => {
+ assert.deepEqual(
+ constructTxParams({
+ selectedToken: true,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }),
+ {
+ 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',
+ selectedToken: false,
+ to: 'mockTo',
+ unapprovedTxs: {
+ '0x123': {},
+ '0x456': {
+ unapprovedTxParam: 'someOtherParam',
+ txParams: {
+ data: 'someData',
+ },
+ },
+ },
+ })
+
+ assert.deepEqual(result, {
+ 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',
+ selectedToken: false,
+ to: 'mockTo',
+ unapprovedTxs: {
+ '0x123': {},
+ '0x456': {
+ unapprovedTxParam: 'someOtherParam',
+ txParams: {
+ from: 'oldFrom',
+ gas: 'oldGas',
+ gasPrice: 'oldGasPrice',
+ },
+ },
+ },
+ })
+
+ assert.deepEqual(result, {
+ unapprovedTxParam: 'someOtherParam',
+ txParams: {
+ from: '0xmockFrom',
+ gas: '0xmockGas',
+ gasPrice: '0xmockGasPrice',
+ value: '0xmockAmount',
+ to: '0xmockTo',
+ },
+ })
+ })
+
+ it('should have token property values if selectedToken is truthy', () => {
+ const result = constructUpdatedTx({
+ amount: 'mockAmount',
+ editingTransactionId: '0x456',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ selectedToken: {
+ address: 'mockTokenAddress',
+ },
+ to: 'mockTo',
+ unapprovedTxs: {
+ '0x123': {},
+ '0x456': {
+ unapprovedTxParam: 'someOtherParam',
+ txParams: {},
+ },
+ },
+ })
+
+ assert.deepEqual(result, {
+ unapprovedTxParam: 'someOtherParam',
+ txParams: {
+ from: '0xmockFrom',
+ gas: '0xmockGas',
+ gasPrice: '0xmockGasPrice',
+ value: '0x0',
+ to: '0xmockTokenAddress',
+ data: `${TOKEN_TRANSFER_FUNCTION_SIGNATURE}ss56Tont`,
+ },
+ })
+ })
+ })
+
+})