diff options
Diffstat (limited to 'ui/app/send.js')
-rw-r--r-- | ui/app/send.js | 541 |
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)) } |