diff --git a/old-ui/app/app.js b/old-ui/app/app.js index d20c80f18..4e46b5606 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -15,6 +15,7 @@ const UnlockScreen = require('./unlock') // accounts const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') +const SendTokenScreen = require('./send-token') const ConfirmTxScreen = require('./conf-tx') // notice const NoticeScreen = require('./components/notice') @@ -577,6 +578,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering send tx screen') return h(SendTransactionScreen, {key: 'send-transaction'}) + case 'sendToken': + log.debug('rendering send tx screen') + return h(SendTokenScreen, {key: 'send-token'}) + case 'newKeychain': log.debug('rendering new keychain screen') return h(NewKeyChainScreen, {key: 'new-keychain'}) diff --git a/old-ui/app/components/eth-balance.js b/old-ui/app/components/eth-balance.js index c05d63781..5173ee46f 100644 --- a/old-ui/app/components/eth-balance.js +++ b/old-ui/app/components/eth-balance.js @@ -16,9 +16,9 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props - const { style, width, network } = props + const { style, width, network, isToken, tokenSymbol } = props var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse, network) : '...' + value = value ? formatBalance(value, 6, needsParse, network, isToken, tokenSymbol) : '...' return ( diff --git a/old-ui/app/components/pending-tx.js b/old-ui/app/components/pending-tx.js index bb54026b9..ac7a499e4 100644 --- a/old-ui/app/components/pending-tx.js +++ b/old-ui/app/components/pending-tx.js @@ -12,6 +12,7 @@ const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const Copyable = require('./copyable') const EthBalance = require('./eth-balance') +const TokenBalance = require('./token-balance') const addressSummary = util.addressSummary const accountSummary = util.accountSummary const nameForAddress = require('../../lib/contract-namer') @@ -20,6 +21,9 @@ const { getEnvironmentType } = require('../../../app/scripts/lib/util') const NetworkIndicator = require('../components/network') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../app/scripts/lib/enums') const connect = require('react-redux').connect +const abiDecoder = require('abi-decoder') +const { tokenInfoGetter, calcTokenAmount } = require('../../../ui/app/token-util') +const BigNumber = require('bignumber.js') const MIN_GAS_PRICE_BN = new BN('0') const MIN_GAS_LIMIT_BN = new BN('21000') @@ -32,7 +36,11 @@ function PendingTx () { valid: true, txData: null, submitting: false, + tokenSymbol: '', + tokenDecimals: 0, + tokenDataRetrieved: false, } + this.tokenInfoGetter = tokenInfoGetter() } function mapStateToProps (state) { @@ -57,12 +65,32 @@ function mapStateToProps (state) { } PendingTx.prototype.render = function () { + const state = this.state + if (!state.tokenDataRetrieved) return null const props = this.props const { currentCurrency, blockGasLimit, network, provider, isUnlocked } = props const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} + let { isToken, tokensToSend, tokensTransferTo } = props + let token = { + address: txParams.to, + } + + const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) + if (decodedData && decodedData.name === 'transfer') { + isToken = true + const tokenValBN = new BigNumber(calcTokenAmount(decodedData.params[1].value, state.tokenDecimals)) + const multiplier = Math.pow(10, 18) + tokensToSend = tokenValBN.mul(multiplier).toString(16) + tokensTransferTo = decodedData.params[0].value + token = { + address: txParams.to, + decimals: state.tokenDecimals, + symbol: state.tokenSymbol, + } + } // Allow retry txs const { lastGasPrice } = txMeta @@ -217,7 +245,10 @@ PendingTx.prototype.render = function () { fontFamily: 'Nunito Regular', }, }, [ - h(EthBalance, { + isToken ? h(TokenBalance, { + token, + fontSize: '12px', + }) : h(EthBalance, { fontSize: '12px', value: balance, conversionRate, @@ -231,7 +262,7 @@ PendingTx.prototype.render = function () { forwardCarrat(), - this.miniAccountPanelForRecipient(), + this.miniAccountPanelForRecipient(isToken, tokensTransferTo), ]), h('style', ` @@ -326,7 +357,17 @@ PendingTx.prototype.render = function () { // in the way that gas and gasLimit currently are. h('.row', [ h('.cell.label', 'Amount'), - h(EthBalance, { valueStyle, dimStyle, value: txParams.value, currentCurrency, conversionRate, network }), + h(EthBalance, { + valueStyle, + dimStyle, + value: isToken ? tokensToSend/* (new BN(tokensToSend)).mul(1e18)*/ : txParams.value, + currentCurrency, + conversionRate, + network, + isToken, + tokenSymbol: this.state.tokenSymbol, + showFiat: !isToken, + }), ]), // Gas Limit (customizable) @@ -381,7 +422,14 @@ PendingTx.prototype.render = function () { // Max Transaction Fee (calculated) h('.cell.row', [ h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { valueStyle, dimStyle, value: txFeeBn.toString(16), currentCurrency, conversionRate, network }), + h(EthBalance, { + valueStyle, + dimStyle, + value: txFeeBn.toString(16), + currentCurrency, + conversionRate, + network, + }), ]), h('.cell.row', { @@ -482,11 +530,12 @@ PendingTx.prototype.render = function () { ) } -PendingTx.prototype.miniAccountPanelForRecipient = function () { +PendingTx.prototype.miniAccountPanelForRecipient = function (isToken, tokensTransferTo) { const props = this.props const txData = props.txData const txParams = txData.txParams || {} const isContractDeploy = !('to' in txParams) + const to = isToken ? tokensTransferTo : txParams.to // If it's not a contract deploy, send to the account if (!isContractDeploy) { @@ -506,17 +555,17 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { display: 'inline-block', whiteSpace: 'nowrap', }, - }, accountSummary(nameForAddress(txParams.to, props.identities)), 6, 4), + }, accountSummary(nameForAddress(to, props.identities)), 6, 4), h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), + value: ethUtil.toChecksumAddress(to), }, [ h('span.font-small', { style: { fontFamily: 'Nunito Regular', color: 'rgba(255, 255, 255, 0.7)', }, - }, addressSummary(txParams.to, 6, 4, false)), + }, addressSummary(to, 6, 4, false)), ]), ]), ]) @@ -536,6 +585,29 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } } +PendingTx.prototype.componentWillMount = function () { + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + this.updateTokenInfo(txParams) +} + +PendingTx.prototype.componentWillUnmount = function () { + this.setState({ + tokenSymbol: '', + tokenDecimals: 0, + tokenDataRetrieved: false, + }) +} + +PendingTx.prototype.updateTokenInfo = async function (txParams) { + const tokenParams = await this.tokenInfoGetter(txParams.to) + this.setState({ + tokenSymbol: tokenParams.symbol, + tokenDecimals: tokenParams.decimals, + tokenDataRetrieved: true, + }) +} + PendingTx.prototype.gasPriceChanged = function (newBN, valid) { log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() diff --git a/old-ui/app/components/token-balance.js b/old-ui/app/components/token-balance.js new file mode 100644 index 000000000..2534110f7 --- /dev/null +++ b/old-ui/app/components/token-balance.js @@ -0,0 +1,143 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-watcher') +const connect = require('react-redux').connect +const selectors = require('../../../ui/app/selectors') +const log = require('loglevel') + +function mapStateToProps (state) { + return { + userAddress: selectors.getSelectedAddress(state), + } +} + +module.exports = connect(mapStateToProps)(TokenBalance) + + +inherits(TokenBalance, Component) +function TokenBalance () { + this.state = { + string: '', + symbol: '', + isLoading: true, + error: null, + } + Component.call(this) +} + +TokenBalance.prototype.render = function () { + const state = this.state + const props = this.props + const { symbol, string, isLoading } = state + const { balanceOnly } = this.props + + const valueStyle = props.valueStyle ? props.valueStyle : { + color: '#ffffff', + width: '100%', + fontSize: props.fontSize || '14px', + textAlign: 'right', + } + const dimStyle = props.dimStyle ? props.dimStyle : { + color: ' #60db97', + fontSize: props.fontSize || '14px', + marginLeft: '5px', + } + + return isLoading + ? h('div', '') + : h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '20px', + textRendering: 'geometricPrecision', + }, + }, [ + h('div.hide-text-overflow.token-balance__amount', { + style: valueStyle, + }, string), + !balanceOnly && h('span.token-balance__symbol', { + style: dimStyle, + }, symbol), + ]) +} + +TokenBalance.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenBalance.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress, token } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: [token], + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalance.bind(this) + this.showError = error => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalance(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenBalance.prototype.componentDidUpdate = function (nextProps) { + const { + userAddress: oldAddress, + token: { address: oldTokenAddress }, + } = this.props + const { + userAddress: newAddress, + token: { address: newTokenAddress }, + } = nextProps + + if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return + if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return + + this.setState({ isLoading: true }) + this.createFreshTokenTracker() +} + +TokenBalance.prototype.updateBalance = function (tokens = []) { + if (!this.tracker.running) { + return + } + + const [{ string, symbol }] = tokens + + this.setState({ + string, + symbol, + isLoading: false, + }) +} + +TokenBalance.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) +} + diff --git a/old-ui/app/components/token-cell.js b/old-ui/app/components/token-cell.js index 6a80ea324..8f7ee6796 100644 --- a/old-ui/app/components/token-cell.js +++ b/old-ui/app/components/token-cell.js @@ -7,11 +7,11 @@ const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem const ethUtil = require('ethereumjs-util') const copyToClipboard = require('copy-to-clipboard') +const actions = require('../../../ui/app/actions') +const connect = require('react-redux').connect const tokenCellDropDownPrefix = 'token-cell_dropdown_' -module.exports = TokenCell - inherits(TokenCell, Component) function TokenCell () { Component.call(this) @@ -76,7 +76,7 @@ TokenCell.prototype.render = function () { } TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) { - const { address, symbol, string, network, userAddress } = this.props + const { address, symbol, string, network, userAddress, showSendTokenPage } = this.props const { optionsMenuActive } = this.state return h( @@ -84,9 +84,9 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) { { style: { position: 'relative', - marginLeft: '-263px', + marginLeft: menuToTop ? '-273px' : '-263px', minWidth: '180px', - marginTop: menuToTop ? '-200px' : '30px', + marginTop: menuToTop ? '-214px' : '30px', width: '280px', }, isOpen: optionsMenuActive, @@ -100,6 +100,16 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) { }, }, [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + showSendTokenPage(address) + }, + }, + `Send`, + ), h( DropdownMenuItem, { @@ -159,3 +169,11 @@ function tokenFactoryFor (tokenAddress) { return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` } +const mapDispatchToProps = dispatch => { + return { + showSendTokenPage: (tokenAddress) => dispatch(actions.showSendTokenPage(tokenAddress)), + } +} + +module.exports = connect(null, mapDispatchToProps)(TokenCell) + diff --git a/old-ui/app/conf-tx.js b/old-ui/app/conf-tx.js index b016afa92..ce798f0f6 100644 --- a/old-ui/app/conf-tx.js +++ b/old-ui/app/conf-tx.js @@ -16,22 +16,28 @@ const Loading = require('./components/loading') module.exports = connect(mapStateToProps)(ConfirmTxScreen) function mapStateToProps (state) { + const { metamask, appState } = state + const { screenParams, pendingTxIndex } = appState.currentView return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - unapprovedTypedMessages: state.metamask.unapprovedTypedMessages, - index: state.appState.currentView.pendingTxIndex || 0, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - computedBalances: state.metamask.computedBalances, + identities: metamask.identities, + accounts: metamask.accounts, + selectedAddress: metamask.selectedAddress, + unapprovedTxs: metamask.unapprovedTxs, + unapprovedMsgs: metamask.unapprovedMsgs, + unapprovedPersonalMsgs: metamask.unapprovedPersonalMsgs, + unapprovedTypedMessages: metamask.unapprovedTypedMessages, + index: pendingTxIndex || 0, + warning: appState.warning, + network: metamask.network, + provider: metamask.provider, + conversionRate: metamask.conversionRate, + currentCurrency: metamask.currentCurrency, + blockGasLimit: metamask.currentBlockGasLimit, + computedBalances: metamask.computedBalances, + isToken: (screenParams && screenParams.isToken), + tokenSymbol: (screenParams && screenParams.tokenSymbol), + tokensToSend: (screenParams && screenParams.tokensToSend), + tokensTransferTo: (screenParams && screenParams.tokensTransferTo), } } @@ -95,6 +101,10 @@ ConfirmTxScreen.prototype.render = function () { unconfTxListLength, computedBalances, network, + isToken: props.isToken, + tokenSymbol: props.tokenSymbol, + tokensToSend: props.tokensToSend, + tokensTransferTo: props.tokensTransferTo, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), sendTransaction: this.sendTransaction.bind(this), diff --git a/old-ui/app/send-token.js b/old-ui/app/send-token.js new file mode 100644 index 000000000..a33489ac4 --- /dev/null +++ b/old-ui/app/send-token.js @@ -0,0 +1,399 @@ +const inherits = require('util').inherits +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('../../ui/app/actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const TokenBalance = require('./components/token-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +const { tokenInfoGetter, calcTokenAmountWithDec } = require('../../ui/app/token-util') +const TokenTracker = require('eth-token-watcher') +const Loading = require('./components/loading') +const BigNumber = require('bignumber.js') +BigNumber.config({ ERRORS: false }) +const log = require('loglevel') + +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, + tokenAddress: state.appState.currentView.tokenAddress, + } + + 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 + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + this.state = { + token: { + address: '', + symbol: '', + balance: 0, + decimals: 0, + }, + isLoading: true, + } + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + const { isLoading, token } = this.state + if (isLoading) { + return h(Loading, { + isLoading: isLoading, + loadingMessage: 'Loading...', + }) + } + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + identity, + network, + identities, + addressBook, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + background: 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))', + padding: '30px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', [ + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', { + style: { + display: 'inline-block', + }, + }, [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.flex-center', { + style: { + color: '#ffffff', + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + color: 'rgba(255, 255, 255, 0.7)', + marginBottom: '30px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + fontSize: '14px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(TokenBalance, { + token, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center', { + style: { + color: '#333333', + marginTop: '18px', + marginBottom: '14px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + position: 'absolute', + left: '30px', + }, + onClick: this.back.bind(this), + }), + `Send ${this.state.token.symbol} Tokens`, + ]), + + // error message + props.error && h('div', {style: { + marginLeft: '30px', + marginRight: '30px', + }}, [ + h('div.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', { + onClick: this.onSubmit.bind(this), + }, 'Next'), + + ]), + ]) + ) +} + +SendTransactionScreen.prototype.componentDidMount = function () { + this.getTokensMetadata() + .then(() => { + this.createFreshTokenTracker() + }) +} + +SendTransactionScreen.prototype.getTokensMetadata = async function () { + this.setState({isLoading: true}) + this.tokenInfoGetter = tokenInfoGetter() + const { tokenAddress, network } = this.props + const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(tokenAddress) + this.setState({ + token: { + address: tokenAddress, + network, + symbol, + decimals, + }, + }) + + return Promise.resolve() +} + +SendTransactionScreen.prototype.componentDidUnmount = function () { + this.props.dispatch(actions.displayWarning('')) + if (!this.tracker) return + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) +} + +SendTransactionScreen.prototype.createFreshTokenTracker = function () { + this.setState({isLoading: true}) + const { address, tokenAddress } = this.props + if (!util.isValidAddress(tokenAddress)) return + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + + this.tracker = new TokenTracker({ + userAddress: address, + provider: global.ethereumProvider, + tokens: [this.state.token], + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +SendTransactionScreen.prototype.updateBalances = function (tokens) { + if (!this.tracker.running) { + return + } + this.setState({ token: (tokens && tokens[0]), isLoading: false }) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = async function () { + const state = this.state || {} + const { token } = state + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const parts = input.split('.') + + let message + + if (isNaN(input) || input === '') { + message = 'Invalid token\'s amount.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (parts[1]) { + var decimal = parts[1] + if (decimal.length > 18) { + message = 'Token\'s amount is too precise.' + return this.props.dispatch(actions.displayWarning(message)) + } + } + + const tokenAddress = ethUtil.addHexPrefix(token.address) + const tokensValueWithoutDec = new BigNumber(input) + const tokensValueWithDec = new BigNumber(calcTokenAmountWithDec(input, token.decimals)) + + if (tokensValueWithDec.gt(token.balance)) { + message = 'Insufficient token\'s balance.' + 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.isInvalidChecksumAddress(recipient))) { + message = 'Recipient address checksum is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!util.isValidAddress(recipient) || (!recipient)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x', + } + + const toAddress = ethUtil.addHexPrefix(recipient) + + txParams.to = tokenAddress + + const tokensAmount = `0x${input.toString(16)}` + const encoded = this.generateTokenTransferData({toAddress, amount: tokensAmount}) + txParams.data = encoded + + const confTxScreenParams = { + isToken: true, + tokenSymbol: token.symbol, + tokensToSend: tokensValueWithoutDec, + tokensTransferTo: toAddress, + } + + this.props.dispatch(actions.signTokenTx(tokenAddress, toAddress, tokensValueWithDec, txParams, confTxScreenParams)) +} + +SendTransactionScreen.prototype.generateTokenTransferData = function ({ toAddress = '0x0', amount = '0x0' }) { + const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb' + const abi = require('ethereumjs-abi') + return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]), + x => ('00' + x.toString(16)).slice(-2) + ).join('') +} diff --git a/old-ui/app/send.js b/old-ui/app/send.js index 2a0d776cf..79567773d 100644 --- a/old-ui/app/send.js +++ b/old-ui/app/send.js @@ -260,7 +260,7 @@ SendTransactionScreen.prototype.onSubmit = function () { const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') const nickname = state.nickname || ' ' const input = document.querySelector('input[name="amount"]').value - const parts = input.split('') + const parts = input.split('.') let message diff --git a/old-ui/app/util.js b/old-ui/app/util.js index f03a7d2bb..518fa1f2d 100644 --- a/old-ui/app/util.js +++ b/old-ui/app/util.js @@ -111,10 +111,11 @@ function parseBalance (balance) { // Takes wei hex, returns an object with three properties. // Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true, network) { +function formatBalance (balance, decimalsToKeep, needsParse = true, network, isToken, tokenSymbol) { const isSokol = parseInt(network) === 77 const isPOA = parseInt(network) === 99 const coinName = isPOA ? 'POA' : isSokol ? 'SPOA' : 'ETH' + const assetName = isToken ? tokenSymbol : coinName var parsed = needsParse ? parseBalance(balance) : balance.split('.') var beforeDecimal = parsed[0] var afterDecimal = parsed[1] @@ -124,14 +125,14 @@ function formatBalance (balance, decimalsToKeep, needsParse = true, network) { if (afterDecimal !== '0') { var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ` ${coinName}` + formatted = '0.' + afterDecimal + ` ${assetName}` } } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${coinName}` + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${assetName}` } } else { afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${coinName}` + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${assetName}` } return formatted } diff --git a/test/e2e/elements.js b/test/e2e/elements.js index 11c44002d..cc420014f 100644 --- a/test/e2e/elements.js +++ b/test/e2e/elements.js @@ -8,9 +8,11 @@ module.exports = { token: { menu: By.id('token-cell_dropdown_0'), items: By.className('dropdown-menu-item'), - view: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(2)'), - copy: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(3)'), - remove: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(4)'), + send: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(2)'), + view: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(3)'), + copy: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(4)'), + remove: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(5)'), + sendText: 'Send', viewText: 'View token on block explorer', copyText: 'Copy address to clipboard', removeText: 'Remove', @@ -25,6 +27,7 @@ module.exports = { }, account: { account1: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(2) > span'), + account2: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'), menu: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div'), delete: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(4) > div.remove'), createAccount: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'), @@ -43,6 +46,29 @@ module.exports = { }, }, screens: { + sendTokens: { + error: By.className('error flex-center'), + errorText: { + invalidAmount: 'Invalid token\'s amount', + address: 'Recipient address is invalid', + largeAmount: 'Insufficient token\'s balance', + tooPrecise: 'Token\'s amount is too precise', + negativeAmount: 'Can not send negative amounts of ETH', + }, + title: By.className('flex-center'), + balance: By.className('hide-text-overflow token-balance__amount'), + symbol: By.className('token-balance__symbol'), + field: { + address: By.name('address'), + addressPlaceholder: 'Recipient Address', + amount: By.name('amount'), + amountPlaceholder: 'Amount', + }, + button: { + next: By.xpath('//*[@id="app-content"]/div/div[4]/div/section[2]/button'), + arrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'), + }, + }, yourPR: { key: By.css('#app-content > div > div.app-primary.from-right > div > div.privateKey > div.flex-row > p'), copy: By.className('clipboard cursor-pointer'), @@ -120,14 +146,17 @@ module.exports = { titleText: 'Delete Custom RPC', }, confirmTransaction: { - buttons: { + title: By.className('flex-row flex-center'), + amount: By.css('#pending-tx-form > div:nth-child(1) > div.table-box > div:nth-child(2) > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'), + symbol: By.css('#pending-tx-form > div:nth-child(1) > div.table-box > div:nth-child(2) > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(2)'), + button: { submit: By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'), }, }, sendTransaction: { title: By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)'), titleText: 'Send Transaction', - fields: { + field: { address: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'), amount: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'), }, @@ -179,7 +208,8 @@ module.exports = { }, main: { identicon: By.className('identicon-wrapper select-none'), - accountName: By.className('sizing-input'), + fieldAccountName: By.className('sizing-input'), + accountName: By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.name-label > div > div > h2'), edit: By.className('edit-text'), iconCopy: By.className('clipboard cursor-pointer white'), transactionList: By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'), @@ -199,7 +229,7 @@ module.exports = { tokens: { menu: By.className('inactiveForm pointer'), token: By.className('token-cell'), - balance: By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3'), + balance: By.css('#token-cell_0 > h3'), amount: By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > div > span'), textNoTokens: 'No tokens found', textYouOwn1token: 'You own 1 token', diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js index ec0453fce..2ff42c1a1 100644 --- a/test/e2e/metamask.spec.js +++ b/test/e2e/metamask.spec.js @@ -134,13 +134,13 @@ describe('Metamask popup page', async function () { await menu.click() const field = await waitUntilShowUp(screens.main.edit) await field.click() - const accountName = await waitUntilShowUp(screens.main.accountName) + const accountName = await waitUntilShowUp(screens.main.fieldAccountName) assert.notEqual(accountName, false, '\'Account name\' change dialog isn\'t opened') assert.equal(await accountName.getAttribute('value'), 'Account 1', 'incorrect account name') }) it('fill out new account\'s name', async () => { - const field = await waitUntilShowUp(screens.main.accountName) + const field = await waitUntilShowUp(screens.main.fieldAccountName) await field.clear() await field.sendKeys(newAccountName) }) @@ -150,7 +150,7 @@ describe('Metamask popup page', async function () { assert.equal(await button.getText(), 'Save', 'button has incorrect name') assert.notEqual(button, true, 'button \'Save\' does not present') await click(button) - const accountName = await waitUntilShowUp(screens.main.accountName, 10) + const accountName = await waitUntilShowUp(screens.main.fieldAccountName, 10) assert.equal(accountName, false, '\'Account name\' change dialog isn\'t opened') }) @@ -587,8 +587,8 @@ describe('Metamask popup page', async function () { it('adds recipient address and amount', async function () { const sendTranscationScreen = await waitUntilShowUp(screens.sendTransaction.title) assert.equal(await sendTranscationScreen.getText(), screens.sendTransaction.titleText, 'Transaction screen has incorrect titlr') - const inputAddress = await waitUntilShowUp(screens.sendTransaction.fields.address) - const inputAmmount = await waitUntilShowUp(screens.sendTransaction.fields.amount) + const inputAddress = await waitUntilShowUp(screens.sendTransaction.field.address) + const inputAmmount = await waitUntilShowUp(screens.sendTransaction.field.amount) await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAmmount.sendKeys('10') const button = await waitUntilShowUp(screens.sendTransaction.buttonNext) @@ -597,7 +597,7 @@ describe('Metamask popup page', async function () { }) it('confirms transaction', async function () { - const button = await waitUntilShowUp(screens.confirmTransaction.buttons.submit) + const button = await waitUntilShowUp(screens.confirmTransaction.button.submit) assert.equal(await button.getAttribute('value'), 'Submit', 'button has incorrect name') await click(button) }) @@ -924,7 +924,7 @@ describe('Metamask popup page', async function () { it('confirms transaction in MetaMask popup', async function () { const windowHandles = await driver.getAllWindowHandles() await driver.switchTo().window(windowHandles[windowHandles.length - 1]) - const button = await waitUntilShowUp(screens.confirmTransaction.buttons.submit) + const button = await waitUntilShowUp(screens.confirmTransaction.button.submit) await click(button) }) @@ -949,6 +949,9 @@ describe('Metamask popup page', async function () { it('navigates to the add token screen', async function () { await waitUntilShowUp(screens.main.identicon) + const tab = await waitUntilShowUp(screens.main.tokens.menu) + await tab.click() + const addTokenButton = await waitUntilShowUp(screens.main.tokens.buttonAdd) assert.equal(await addTokenButton.getText(), screens.main.tokens.buttonAddText) await click(addTokenButton) @@ -1009,6 +1012,50 @@ describe('Metamask popup page', async function () { await switchToFirstPage() }) }) + describe('Token menu', function () { + + it('token menu is displayed and clickable ', async function () { + const menu = await waitUntilShowUp(menus.token.menu) + await menu.click() + }) + + it('link \'View on blockexplorer...\' leads to correct page ', async function () { + const menu = await waitUntilShowUp(menus.token.view) + assert.notEqual(menu, false, 'item isn\'t displayed') + assert.equal(await menu.getText(), menus.token.viewText, 'incorrect name') + await menu.click() + await switchToLastPage() + const title = await driver.getCurrentUrl() + assert.equal(title.includes('https://etherscan.io/token/'), true, 'link leads to wrong page') + await switchToFirstPage() + }) + + it('item \'Copy\' is displayed and clickable ', async function () { + let menu = await waitUntilShowUp(menus.token.menu) + await menu.click() + const item = await waitUntilShowUp(menus.token.copy) + assert.notEqual(item, false, 'item isn\'t displayed') + assert.equal(await item.getText(), menus.token.copyText, 'incorrect name') + await item.click() + menu = await waitUntilShowUp(menus.token.menu, 10) + assert.notEqual(menu, false, 'menu wasn\'t closed') + }) + + it('item \'Remove\' is displayed', async function () { + const menu = await waitUntilShowUp(menus.token.menu) + await menu.click() + const item = await waitUntilShowUp(menus.token.remove) + assert.notEqual(item, false, 'item isn\'t displayed') + assert.equal(await item.getText(), menus.token.removeText, 'incorrect name') + }) + + it('item \'Send \' is displayed', async function () { + const item = await waitUntilShowUp(menus.token.send) + assert.notEqual(item, false, 'item isn\'t displayed') + assert.equal(await item.getText(), menus.token.sendText, 'incorrect name') + await waitUntilShowUp(menus.token.menu) + }) + }) describe('Check support of token per network basis ', async function () { @@ -1092,44 +1139,178 @@ describe('Metamask popup page', async function () { }) }) - describe('Token menu', function () { + describe('Transfer tokens', function () { - it('token menu is displayed and clickable ', async function () { - const menu = await waitUntilShowUp(menus.token.menu) - await menu.click() - }) - - it('link \'View on blockexplorer...\' leads to correct page ', async function () { - const menu = await waitUntilShowUp(menus.token.view) - assert.notEqual(menu, false, 'item isn\'t displayed') - assert.equal(await menu.getText(), menus.token.viewText, 'incorrect name') - await menu.click() - await switchToLastPage() - const title = await driver.getCurrentUrl() - assert.equal(title.includes('https://etherscan.io/token/'), true, 'link leads to wrong page') - await switchToFirstPage() - }) - - it('item \'Copy\' is displayed and clickable ', async function () { - let menu = await waitUntilShowUp(menus.token.menu) - await menu.click() - const item = await waitUntilShowUp(menus.token.copy) - assert.notEqual(item, false, 'item isn\'t displayed') - assert.equal(await item.getText(), menus.token.copyText, 'incorrect name') + const account2 = '0x2f318C334780961FB129D2a6c30D0763d9a5C970' + const invalidAddress = '0xkqjefwblknnecwe' + const invalidAmount = 'eeeee' + const largeAmount = '123' + const preciseAmount = '0.123456789123456789123' + const negativeAmount = '-1' + it('switch to account 1 ', async function () { + const accountMenu = await waitUntilShowUp(menus.account.menu) + await accountMenu.click() + const item = await waitUntilShowUp(menus.account.account1) await item.click() - menu = await waitUntilShowUp(menus.token.menu, 10) - assert.notEqual(menu, false, 'menu wasn\'t closed') + await delay(2000) + const accountName = await waitUntilShowUp(screens.main.accountName) + assert.equal(await accountName.getText(), 'Account 1', 'account name incorrect') }) - it('item \'Remove\' is displayed', async function () { + it('open screen \'Transfer tokens\' ', async function () { const menu = await waitUntilShowUp(menus.token.menu) await menu.click() - const item = await waitUntilShowUp(menus.token.remove) - assert.notEqual(item, false, 'item isn\'t displayed') - assert.equal(await item.getText(), menus.token.removeText, 'incorrect name') + const item = await waitUntilShowUp(menus.token.send) + await item.click() + }) + + it('field \'Amount\' is displayed and has correct placeholder ', async function () { + const item = await waitUntilShowUp(screens.sendTokens.field.amount) + assert.equal(await item.getAttribute('placeholder'), screens.sendTokens.field.amountPlaceholder, 'placeholder is incorrect') + }) + + it('field \'Address\' is displayed and has correct placeholder ', async function () { + const item = await waitUntilShowUp(screens.sendTokens.field.address) + assert.equal(await item.getAttribute('placeholder'), screens.sendTokens.field.addressPlaceholder, 'placeholder is incorrect') + }) + + it('token\'s balance is correct ', async function () { + const item = await waitUntilShowUp(screens.sendTokens.balance) + assert.equal(await item.getText(), '100', 'token\'s balance is incorrect') + }) + + it('token\'s symbol is correct ', async function () { + const item = await waitUntilShowUp(screens.sendTokens.symbol) + assert.equal(await item.getText(), 'TST', 'token\'s symbol is incorrect') + }) + + it('error message if invalid token\'s amount', async function () { + const button = await waitUntilShowUp(screens.sendTokens.button.next) + assert.equal(await button.getText(), 'Next', 'button \'Next\' has incorrect name') + await click(button) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.invalidAmount, ' error message is incorrect') + }) + + it('error message if invalid address', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await amount.sendKeys('1') + const address = await waitUntilShowUp(screens.sendTokens.field.address) + await address.sendKeys(invalidAddress) + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + await click(button) + await delay(2000) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.address, ' error message is incorrect') + }) + + it('error message if amount is large', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await amount.sendKeys(largeAmount) + const address = await waitUntilShowUp(screens.sendTokens.field.address) + await clearField(address) + await address.sendKeys(account2) + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + await click(button) + await delay(2000) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.largeAmount, ' error message is incorrect') + }) + + it('error message if amount is invalid', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await clearField(amount) + await amount.sendKeys(invalidAmount) + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + await click(button) + await delay(2000) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.invalidAmount, ' error message is incorrect') + }) + it.skip('error message if amount is too precise', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await clearField(amount) + await amount.sendKeys(preciseAmount) + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + await click(button) + await delay(2000) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.tooPrecise, ' error message is incorrect') + }) + + it('error message if amount is negative', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await clearField(amount) + await amount.sendKeys(negativeAmount) + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + await click(button) + await delay(2000) + const error = await waitUntilShowUp(screens.sendTokens.error) + assert.equal(await error.getText(), screens.sendTokens.errorText.negativeAmount, ' error message is incorrect') + }) + + it('\'Confirm transaction\' screen is opened if address and amount are correct', async function () { + const amount = await waitUntilShowUp(screens.sendTokens.field.amount) + await clearField(amount) + await amount.sendKeys('5') + const button = await waitUntilShowUp(screens.sendTokens.button.next) + await click(button) + + const buttonSubmit = await waitUntilShowUp(screens.confirmTransaction.button.submit) + assert.notEqual(buttonSubmit, false, 'incorrect screen was opened') + }) + + it('\'Confirm transaction\' screen: token\'s amount is correct', async function () { + const amount = await waitUntilShowUp(screens.confirmTransaction.amount) + assert.equal(await amount.getText(), '5.000', ' amount is incorrect') + }) + + it('\'Confirm transaction\' screen: token\'s symbol is correct', async function () { + const symbol = await waitUntilShowUp(screens.confirmTransaction.symbol) + assert.equal(await symbol.getText(), 'TST', ' symbol is incorrect') + }) + + it('submit transaction', async function () { + await driver.navigate().refresh() + const button = await waitUntilShowUp(screens.confirmTransaction.button.submit) + await click(button) + const list = await waitUntilShowUp(screens.main.transactionList) + assert.notEqual(list, false, ' main screen isn\'t opened') + }) + + it('correct amount substracted from sender\'s tokens balance', async function () { + const tab = await waitUntilShowUp(screens.main.tokens.menu) + await tab.click() + await driver.navigate().refresh() + await delay(5000) + await driver.navigate().refresh() + await delay(5000) + await driver.navigate().refresh() + await delay(5000) + const balance = await waitUntilShowUp(screens.main.tokens.balance) + + assert.equal(await balance.getText(), '95 TST', 'balance is incorrect') + }) + it('switch to account 2 ', async function () { + const accountMenu = await waitUntilShowUp(menus.account.menu) + await accountMenu.click() + const item = await waitUntilShowUp(menus.account.account2) + await item.click() + await delay(2000) + const accountName = await waitUntilShowUp(screens.main.accountName) + assert.equal(await accountName.getText(), 'Account 2', 'account name incorrect') + }) + + it('receiver got correct amount of tokens', async function () { + const balance = await waitUntilShowUp(screens.main.tokens.balance) + assert.equal(await balance.getText(), '5 TST', 'balance is incorrect') }) }) - describe('Remove token , provider is localhost', function () { it('remove option opens \'Remove token\' screen ', async function () { diff --git a/ui/app/actions.js b/ui/app/actions.js index 8a9b6d47b..76daffeb5 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1074,7 +1074,7 @@ function sendTx (txData) { } } -function signTokenTx (tokenAddress, toAddress, amount, txData) { +function signTokenTx (tokenAddress, toAddress, amount, txData, confTxScreenParams) { return dispatch => { dispatch(actions.showLoadingIndication()) const token = global.eth.contract(abi).at(tokenAddress) @@ -1083,7 +1083,7 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) { dispatch(actions.hideLoadingIndication()) dispatch(actions.displayWarning(err.message)) }) - dispatch(actions.showConfTxPage({})) + dispatch(actions.showConfTxPage(confTxScreenParams || {})) } } @@ -1109,7 +1109,7 @@ function updateTransaction (txData) { .then(() => updateMetamaskStateFromBackground()) .then(newState => dispatch(actions.updateMetamaskState(newState))) .then(() => { - dispatch(actions.showConfTxPage({ id: txData.id })) + dispatch(actions.showConfTxPage({ id: txData.id})) dispatch(actions.hideLoadingIndication()) return txData }) @@ -1143,6 +1143,7 @@ function updateAndApproveTx (txData) { dispatch(actions.clearSend()) dispatch(actions.completedTx(txData.id)) dispatch(actions.hideLoadingIndication()) + dispatch(actions.setCurrentAccountTab('history')) if (!hasUnconfirmedTransactions(getState())) { return global.platform.closeNotificationWindow() @@ -1542,11 +1543,12 @@ function showAccountsPage () { } } -function showConfTxPage ({transForward = true, id}) { +function showConfTxPage (screenParams) { return { type: actions.SHOW_CONF_TX_PAGE, - transForward, - id, + transForward: (screenParams.transForward || true), + id: screenParams.id, + value: screenParams, } } @@ -1994,9 +1996,11 @@ function showSendPage () { } } -function showSendTokenPage () { + +function showSendTokenPage (address) { return { type: actions.SHOW_SEND_TOKEN_PAGE, + value: address, } } diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 743ee06ec..009d607c6 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -122,7 +122,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa if (isUnapproved) { opts.onClick = () => { - this.props.showConfTxPage({ id: transactionId }) + this.props.showConfTxPage({ id: transactionId}) history.push(CONFIRM_TRANSACTION_ROUTE) } opts.transactionStatus = this.context.t('notStarted') diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index d710863b0..0f1be78bf 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -297,6 +297,7 @@ function reduceApp (state, action) { currentView: { name: 'sendToken', context: appState.currentView.context, + tokenAddress: action.value, }, transForward: true, warning: null, @@ -438,6 +439,7 @@ function reduceApp (state, action) { currentView: { name: 'confTx', pendingTxIndex: action.id ? indexForPending(state, action.id) : 0, + screenParams: action.value, }, transForward: action.transForward, warning: null, diff --git a/ui/app/token-util.js b/ui/app/token-util.js index 0d4233766..b358bca98 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -47,9 +47,15 @@ function calcTokenAmount (value, decimals) { return new BigNumber(value).div(multiplier).toNumber() } +function calcTokenAmountWithDec (valueWithoutDec, decimals) { + const multiplier = Math.pow(10, Number(decimals || 0)) + return new BigNumber(valueWithoutDec).mul(multiplier).toNumber() +} + module.exports = { tokenInfoGetter, calcTokenAmount, + calcTokenAmountWithDec, getSymbolAndDecimals, } diff --git a/ui/index.js b/ui/index.js index bd9ecc28b..8b8d6d6e7 100644 --- a/ui/index.js +++ b/ui/index.js @@ -54,6 +54,7 @@ async function startApp (metamaskState, accountManager, opts) { const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const numberOfUnapprivedTx = unapprovedTxsAll.length if (numberOfUnapprivedTx > 0) { + store.dispatch(actions.showConfTxPage({ id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id, }))