aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/send.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/send.js')
-rw-r--r--ui/app/send.js541
1 files changed, 342 insertions, 199 deletions
diff --git a/ui/app/send.js b/ui/app/send.js
index a21a219eb..b14c48e56 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -1,42 +1,89 @@
-const inherits = require('util').inherits
+const { inherits } = require('util')
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const Identicon = require('./components/identicon')
-const actions = require('./actions')
-const util = require('./util')
-const numericBalance = require('./util').numericBalance
-const addressSummary = require('./util').addressSummary
-const isHex = require('./util').isHex
-const EthBalance = require('./components/eth-balance')
const EnsInput = require('./components/ens-input')
-const ethUtil = require('ethereumjs-util')
+const GasTooltip = require('./components/send/gas-tooltip')
+const CurrencyToggle = require('./components/send/currency-toggle')
+const GasFeeDisplay = require('./components/send/gas-fee-display')
+const { getSelectedIdentity } = require('./selectors')
+
+const {
+ showAccountsPage,
+ backToAccountDetail,
+ displayWarning,
+ hideWarning,
+ addToAddressBook,
+ signTx,
+} = require('./actions')
+const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util')
+const { isHex, numericBalance } = require('./util')
+const { conversionUtil } = require('./conversion-util')
+const BigNumber = require('bignumber.js')
+
+const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0'
+
module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps (state) {
- var result = {
- address: state.metamask.selectedAddress,
- accounts: state.metamask.accounts,
- identities: state.metamask.identities,
- warning: state.appState.warning,
- network: state.metamask.network,
- addressBook: state.metamask.addressBook,
- conversionRate: state.metamask.conversionRate,
- currentCurrency: state.metamask.currentCurrency,
- }
-
- result.error = result.warning && result.warning.split('.')[0]
-
- result.account = result.accounts[result.address]
- result.identity = result.identities[result.address]
- result.balance = result.account ? numericBalance(result.account.balance) : null
+ const {
+ selectedAddress: address,
+ accounts,
+ identities,
+ network,
+ addressBook,
+ conversionRate,
+ currentBlockGasLimit: blockGasLimit,
+ } = state.metamask
+ const { warning } = state.appState
+ const selectedIdentity = getSelectedIdentity(state)
+ const account = accounts[address]
- return result
+ return {
+ address,
+ accounts,
+ identities,
+ network,
+ addressBook,
+ conversionRate,
+ blockGasLimit,
+ warning,
+ selectedIdentity,
+ error: warning && warning.split('.')[0],
+ account,
+ identity: identities[address],
+ balance: account ? numericBalance(account.balance) : null,
+ }
}
inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () {
PersistentForm.call(this)
+
+ // [WIP] These are the bare minimum of tx props needed to sign a transaction
+ // We will need a few more for contract-related interactions
+ this.state = {
+ newTx: {
+ from: '',
+ to: '',
+ // these values are hardcoded, so "Next" can be clicked
+ amount: '0x0', // see L544
+ gasPrice: '0x5d21dba00',
+ gas: '0x7b0d',
+ txData: null,
+ memo: '',
+ },
+ activeCurrency: 'USD',
+ tooltipIsOpen: false,
+ }
+
+ this.back = this.back.bind(this)
+ this.closeTooltip = this.closeTooltip.bind(this)
+ this.onSubmit = this.onSubmit.bind(this)
+ this.recipientDidChange = this.recipientDidChange.bind(this)
+ this.setActiveCurrency = this.setActiveCurrency.bind(this)
+ this.toggleTooltip = this.toggleTooltip.bind(this)
}
SendTransactionScreen.prototype.render = function () {
@@ -44,195 +91,260 @@ SendTransactionScreen.prototype.render = function () {
const props = this.props
const {
- address,
- account,
- identity,
- network,
- identities,
- addressBook,
+ // selectedIdentity,
+ // network,
+ // identities,
+ // addressBook,
conversionRate,
- currentCurrency,
} = props
+ const { blockGasLimit, newTx, activeCurrency } = this.state
+ const { gas, gasPrice } = newTx
+ // console.log(`activeCurrency`, activeCurrency)
+ // console.log({ selectedIdentity, identities })
+ // console.log('SendTransactionScreen state:', this.state)
+
return (
- h('.send-screen.flex-column.flex-grow', [
-
- //
- // Sender Profile
- //
-
- h('.account-data-subsection.flex-row.flex-grow', {
- style: {
- margin: '0 20px',
- },
- }, [
-
- // header - identicon + nav
- h('.flex-row.flex-space-between', {
- style: {
- marginTop: '15px',
- },
- }, [
- // back button
- h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: this.back.bind(this),
+ h('div.send-screen-wrapper', [
+ // Main Send token Card
+ h('div.send-screen-card', [
+
+ h('img.send-eth-icon', { src: '../images/eth_logo.svg' }),
+
+ h('div.send-screen__title', 'Send'),
+
+ h('div.send-screen__subtitle', 'Send Ethereum to anyone with an Ethereum account'),
+
+ h('div.send-screen-input-wrapper', [
+
+ h('div', 'From:'),
+
+ h('input.large-input.send-screen-input', {
+ list: 'accounts',
+ placeholder: 'Account',
+ value: this.state.newTx.from,
+ onChange: (event) => {
+ console.log('event', event.target.value)
+ this.setState({
+ newTx: {
+ ...this.state.newTx,
+ from: event.target.value,
+ },
+ })
+ },
}),
- // large identicon
- h('.identicon-wrapper.flex-column.flex-center.select-none', [
- h(Identicon, {
- diameter: 62,
- address: address,
+ h('datalist#accounts', [
+ Object.keys(props.identities).map((key) => {
+ const identity = props.identities[key]
+ return h('option', {
+ value: identity.address,
+ label: identity.name,
+ key: identity.address,
+ })
}),
]),
- // invisible place holder
- h('i.fa.fa-users.fa-lg.invisible', {
- style: {
- marginTop: '28px',
+ ]),
+
+ h('div.send-screen-input-wrapper', [
+
+ h('div', 'To:'),
+
+ h('input.large-input.send-screen-input', {
+ name: 'address',
+ list: 'addresses',
+ placeholder: 'Address',
+ value: this.state.newTx.to,
+ onChange: (event) => {
+ console.log('event', event.target.value)
+ this.setState({
+ newTx: {
+ ...this.state.newTx,
+ to: event.target.value,
+ },
+ })
},
}),
+ h('datalist#addresses', [
+ // Corresponds to the addresses owned.
+ Object.entries(props.identities).map(([key, { address, name }]) => {
+ return h('option', {
+ value: address,
+ label: name,
+ key: address,
+ })
+ }),
+ // Corresponds to previously sent-to addresses.
+ props.addressBook.map(({ address, name }) => {
+ return h('option', {
+ value: address,
+ label: name,
+ key: address,
+ })
+ }),
+ ]),
+
+ // h(EnsInput, {
+ // name: 'address',
+ // placeholder: 'Recipient Address',
+ // value: this.state.newTx.to,
+ // onChange: (event) => {
+ // this.setState({
+ // newTx: Object.assign(
+ // this.state.newTx,
+ // {
+ // to: event.target.value,
+ // }
+ // ),
+ // })
+ // },
+ // network,
+ // identities,
+ // addressBook,
+ // }),
+
]),
- // account label
-
- h('.flex-column', {
- style: {
- marginTop: '10px',
- alignItems: 'flex-start',
- },
- }, [
- h('h2.font-medium.color-forest.flex-center', {
- style: {
- paddingTop: '8px',
- marginBottom: '8px',
- },
- }, identity && identity.name),
+ h('div.send-screen-input-wrapper', [
+
+ h('div.send-screen-amount-labels', [
+ h('span', 'Amount'),
+ h(CurrencyToggle, {
+ activeCurrency,
+ onClick: (newCurrency) => this.setActiveCurrency(newCurrency),
+ }), // holding on icon from design
+ ]),
- // address and getter actions
- h('.flex-row.flex-center', {
- style: {
- marginBottom: '8px',
+ h('input.large-input.send-screen-input', {
+ placeholder: `0 ${activeCurrency}`,
+ type: 'number',
+ onChange: (event) => {
+ this.setState({
+ newTx: Object.assign(
+ this.state.newTx,
+ {
+ amount: event.target.value,
+ }
+ ),
+ })
},
- }, [
+ }),
- h('div', {
- style: {
- lineHeight: '16px',
- },
- }, addressSummary(address)),
+ ]),
- ]),
+ h('div.send-screen-input-wrapper', [
+ this.state.tooltipIsOpen && h(GasTooltip, {
+ className: 'send-tooltip',
+ gasPrice,
+ gasLimit: gas,
+ onClose: this.closeTooltip,
+ onFeeChange: ({gasLimit, gasPrice}) => {
+ this.setState({
+ newTx: {
+ ...this.state.newTx,
+ gas: gasLimit,
+ gasPrice,
+ },
+ })
+ },
+ }),
- // balance
- h('.flex-row.flex-center', [
+ h('div.send-screen-gas-labels', [
+ h('span', [
+ h('i.fa.fa-bolt'),
+ 'Gas fee:',
+ ]),
+ h('span', 'What\'s this?'),
+ ]),
- h(EthBalance, {
- value: account && account.balance,
+ // TODO: handle loading time when switching to USD
+ h('div.large-input.send-screen-gas-input', {}, [
+ h(GasFeeDisplay, {
+ activeCurrency,
conversionRate,
- currentCurrency,
+ gas,
+ gasPrice,
+ blockGasLimit,
}),
-
+ h('div.send-screen-gas-input-customize', {
+ onClick: this.toggleTooltip,
+ }, [
+ 'Customize',
+ ]),
]),
- ]),
- ]),
-
- //
- // Required Fields
- //
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginTop: '15px',
- marginBottom: '16px',
- },
- }, [
- 'Send Transaction',
- ]),
- // error message
- props.error && h('span.error.flex-center', props.error),
-
- // 'to' field
- h('section.flex-row.flex-center', [
- h(EnsInput, {
- name: 'address',
- placeholder: 'Recipient Address',
- onChange: this.recipientDidChange.bind(this),
- network,
- identities,
- addressBook,
- }),
- ]),
-
- // 'amount' and send button
- h('section.flex-row.flex-center', [
-
- h('input.large-input', {
- name: 'amount',
- placeholder: 'Amount',
- type: 'number',
- style: {
- marginRight: '6px',
- },
- dataset: {
- persistentFormId: 'tx-amount',
- },
- }),
-
- h('button.primary', {
- onClick: this.onSubmit.bind(this),
- style: {
- textTransform: 'uppercase',
- },
- }, 'Next'),
+ ]),
- ]),
+ h('div.send-screen-input-wrapper', [
+ h('div', 'Transaction memo (optional)'),
+ h('input.large-input.send-screen-input', {
+ onChange: () => {
+ this.setState({
+ newTx: Object.assign(
+ this.state.newTx,
+ {
+ memo: event.target.value,
+ }
+ ),
+ })
+ },
+ }),
+ ]),
- //
- // Optional Fields
- //
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginTop: '16px',
- marginBottom: '16px',
- },
- }, [
- 'Transaction Data (optional)',
+ h('div.send-screen-input-wrapper', {}, [
+ h('div', {}, ['Data (optional)']),
+ h('input.large-input.send-screen-input', {
+ onChange: () => {
+ this.setState({
+ newTx: Object.assign(
+ this.state.newTx,
+ {
+ txData: event.target.value,
+ }
+ ),
+ })
+ },
+ }),
+ ]),
]),
- // 'data' field
+ // Buttons underneath card
h('section.flex-column.flex-center', [
- h('input.large-input', {
- name: 'txData',
- placeholder: '0x01234',
- style: {
- width: '100%',
- resize: 'none',
- },
- dataset: {
- persistentFormId: 'tx-data',
- },
- }),
+ h('button.btn-secondary.send-screen__send-button', {
+ onClick: (event) => this.onSubmit(event),
+ }, 'Next'),
+ h('button.btn-tertiary.send-screen__cancel-button', {
+ onClick: this.back,
+ }, 'Cancel'),
]),
])
+
)
}
+SendTransactionScreen.prototype.toggleTooltip = function () {
+ this.setState({ tooltipIsOpen: !this.state.tooltipIsOpen })
+}
+
+SendTransactionScreen.prototype.closeTooltip = function () {
+ this.setState({ tooltipIsOpen: false })
+}
+
+SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) {
+ this.setState({ activeCurrency: newCurrency })
+}
+
SendTransactionScreen.prototype.navigateToAccounts = function (event) {
event.stopPropagation()
- this.props.dispatch(actions.showAccountsPage())
+ this.props.dispatch(showAccountsPage())
}
SendTransactionScreen.prototype.back = function () {
var address = this.props.address
- this.props.dispatch(actions.backToAccountDetail(address))
+ this.props.dispatch(backToAccountDetail(address))
}
SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) {
@@ -242,47 +354,78 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna
})
}
-SendTransactionScreen.prototype.onSubmit = function () {
+SendTransactionScreen.prototype.onSubmit = function (event) {
+ event.preventDefault()
const state = this.state || {}
- const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
+
+ // const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
+ const recipient = state.newTx.to
+
const nickname = state.nickname || ' '
- const input = document.querySelector('input[name="amount"]').value
- const value = util.normalizeEthStringToWei(input)
- const txData = document.querySelector('input[name="txData"]').value
- const balance = this.props.balance
+
+ // const input = document.querySelector('input[name="amount"]').value
+ // const input = state.newTx.value
+ // const value = util.normalizeEthStringToWei(input)
+
+ // https://consensys.slack.com/archives/G1L7H42BT/p1503439134000169?thread_ts=1503438076.000411&cid=G1L7H42BT
+ // From @kumavis: "not needed for MVP but we will end up adding it again so consider just adding it now"
+ const txData = false
+ // Must replace with memo data.
+ // const txData = document.querySelector('input[name="txData"]').value
+
let message
- if (value.gt(balance)) {
- message = 'Insufficient funds.'
- return this.props.dispatch(actions.displayWarning(message))
- }
+ // if (value.gt(balance)) {
+ // message = 'Insufficient funds.'
+ // return this.props.dispatch(actions.displayWarning(message))
+ // }
- if (input < 0) {
- message = 'Can not send negative amounts of ETH.'
- return this.props.dispatch(actions.displayWarning(message))
- }
+ // if (input < 0) {
+ // message = 'Can not send negative amounts of ETH.'
+ // return this.props.dispatch(actions.displayWarning(message))
+ // }
- if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
- message = 'Recipient address is invalid.'
- return this.props.dispatch(actions.displayWarning(message))
- }
+ // if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
+ // message = 'Recipient address is invalid.'
+ // return this.props.dispatch(actions.displayWarning(message))
+ // }
- if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) {
+ if (txData && !isHex(stripHexPrefix(txData))) {
message = 'Transaction data must be hex string.'
- return this.props.dispatch(actions.displayWarning(message))
+ return this.props.dispatch(displayWarning(message))
}
- this.props.dispatch(actions.hideWarning())
+ this.props.dispatch(hideWarning())
+
+ this.props.dispatch(addToAddressBook(recipient, nickname))
- this.props.dispatch(actions.addToAddressBook(recipient, nickname))
+ // TODO: need a clean way to integrate this into conversionUtil
+ const sendConversionRate = state.activeCurrency === 'ETH'
+ ? this.props.conversionRate
+ : new BigNumber(1.0).div(this.props.conversionRate)
+ const sendAmount = conversionUtil(this.state.newTx.amount, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromCurrency: state.activeCurrency,
+ toCurrency: 'ETH',
+ toDenomination: 'WEI',
+ conversionRate: sendConversionRate,
+ })
+
var txParams = {
- from: this.props.address,
- value: '0x' + value.toString(16),
+ from: this.state.newTx.from,
+ to: this.state.newTx.to,
+
+ value: sendAmount,
+
+ // New: gas will now be specified on this step
+ gas: this.state.newTx.gas,
+ gasPrice: this.state.newTx.gasPrice,
}
- if (recipient) txParams.to = ethUtil.addHexPrefix(recipient)
+ if (recipient) txParams.to = addHexPrefix(recipient)
if (txData) txParams.data = txData
- this.props.dispatch(actions.signTx(txParams))
+ this.props.dispatch(signTx(txParams))
}