Merge remote-tracking branch 'origin/develop' into add-tokens-validation

This commit is contained in:
Victor Baranov 2018-10-16 18:06:02 +03:00
commit 53b3802396
16 changed files with 959 additions and 87 deletions

View File

@ -15,6 +15,7 @@ const UnlockScreen = require('./unlock')
// accounts // accounts
const AccountDetailScreen = require('./account-detail') const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send') const SendTransactionScreen = require('./send')
const SendTokenScreen = require('./send-token')
const ConfirmTxScreen = require('./conf-tx') const ConfirmTxScreen = require('./conf-tx')
// notice // notice
const NoticeScreen = require('./components/notice') const NoticeScreen = require('./components/notice')
@ -577,6 +578,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering send tx screen') log.debug('rendering send tx screen')
return h(SendTransactionScreen, {key: 'send-transaction'}) return h(SendTransactionScreen, {key: 'send-transaction'})
case 'sendToken':
log.debug('rendering send tx screen')
return h(SendTokenScreen, {key: 'send-token'})
case 'newKeychain': case 'newKeychain':
log.debug('rendering new keychain screen') log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'}) return h(NewKeyChainScreen, {key: 'new-keychain'})

View File

@ -16,9 +16,9 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () { EthBalanceComponent.prototype.render = function () {
var props = this.props var props = this.props
let { value } = 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 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 ( return (

View File

@ -12,6 +12,7 @@ const util = require('../util')
const MiniAccountPanel = require('./mini-account-panel') const MiniAccountPanel = require('./mini-account-panel')
const Copyable = require('./copyable') const Copyable = require('./copyable')
const EthBalance = require('./eth-balance') const EthBalance = require('./eth-balance')
const TokenBalance = require('./token-balance')
const addressSummary = util.addressSummary const addressSummary = util.addressSummary
const accountSummary = util.accountSummary const accountSummary = util.accountSummary
const nameForAddress = require('../../lib/contract-namer') const nameForAddress = require('../../lib/contract-namer')
@ -20,6 +21,9 @@ const { getEnvironmentType } = require('../../../app/scripts/lib/util')
const NetworkIndicator = require('../components/network') const NetworkIndicator = require('../components/network')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../app/scripts/lib/enums') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../app/scripts/lib/enums')
const connect = require('react-redux').connect 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_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000') const MIN_GAS_LIMIT_BN = new BN('21000')
@ -32,7 +36,11 @@ function PendingTx () {
valid: true, valid: true,
txData: null, txData: null,
submitting: false, submitting: false,
tokenSymbol: '',
tokenDecimals: 0,
tokenDataRetrieved: false,
} }
this.tokenInfoGetter = tokenInfoGetter()
} }
function mapStateToProps (state) { function mapStateToProps (state) {
@ -57,12 +65,32 @@ function mapStateToProps (state) {
} }
PendingTx.prototype.render = function () { PendingTx.prototype.render = function () {
const state = this.state
if (!state.tokenDataRetrieved) return null
const props = this.props const props = this.props
const { currentCurrency, blockGasLimit, network, provider, isUnlocked } = props const { currentCurrency, blockGasLimit, network, provider, isUnlocked } = props
const conversionRate = props.conversionRate const conversionRate = props.conversionRate
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {} 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 // Allow retry txs
const { lastGasPrice } = txMeta const { lastGasPrice } = txMeta
@ -217,7 +245,10 @@ PendingTx.prototype.render = function () {
fontFamily: 'Nunito Regular', fontFamily: 'Nunito Regular',
}, },
}, [ }, [
h(EthBalance, { isToken ? h(TokenBalance, {
token,
fontSize: '12px',
}) : h(EthBalance, {
fontSize: '12px', fontSize: '12px',
value: balance, value: balance,
conversionRate, conversionRate,
@ -231,7 +262,7 @@ PendingTx.prototype.render = function () {
forwardCarrat(), forwardCarrat(),
this.miniAccountPanelForRecipient(), this.miniAccountPanelForRecipient(isToken, tokensTransferTo),
]), ]),
h('style', ` h('style', `
@ -326,7 +357,17 @@ PendingTx.prototype.render = function () {
// in the way that gas and gasLimit currently are. // in the way that gas and gasLimit currently are.
h('.row', [ h('.row', [
h('.cell.label', 'Amount'), 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) // Gas Limit (customizable)
@ -381,7 +422,14 @@ PendingTx.prototype.render = function () {
// Max Transaction Fee (calculated) // Max Transaction Fee (calculated)
h('.cell.row', [ h('.cell.row', [
h('.cell.label', 'Max Transaction Fee'), 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', { 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 props = this.props
const txData = props.txData const txData = props.txData
const txParams = txData.txParams || {} const txParams = txData.txParams || {}
const isContractDeploy = !('to' in txParams) const isContractDeploy = !('to' in txParams)
const to = isToken ? tokensTransferTo : txParams.to
// If it's not a contract deploy, send to the account // If it's not a contract deploy, send to the account
if (!isContractDeploy) { if (!isContractDeploy) {
@ -506,17 +555,17 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () {
display: 'inline-block', display: 'inline-block',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}, },
}, accountSummary(nameForAddress(txParams.to, props.identities)), 6, 4), }, accountSummary(nameForAddress(to, props.identities)), 6, 4),
h(Copyable, { h(Copyable, {
value: ethUtil.toChecksumAddress(txParams.to), value: ethUtil.toChecksumAddress(to),
}, [ }, [
h('span.font-small', { h('span.font-small', {
style: { style: {
fontFamily: 'Nunito Regular', fontFamily: 'Nunito Regular',
color: 'rgba(255, 255, 255, 0.7)', 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) { PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
log.info(`Gas price changed to: ${newBN.toString(10)}`) log.info(`Gas price changed to: ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()

View File

@ -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)
}

View File

@ -7,11 +7,11 @@ const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const copyToClipboard = require('copy-to-clipboard') const copyToClipboard = require('copy-to-clipboard')
const actions = require('../../../ui/app/actions')
const connect = require('react-redux').connect
const tokenCellDropDownPrefix = 'token-cell_dropdown_' const tokenCellDropDownPrefix = 'token-cell_dropdown_'
module.exports = TokenCell
inherits(TokenCell, Component) inherits(TokenCell, Component)
function TokenCell () { function TokenCell () {
Component.call(this) Component.call(this)
@ -76,7 +76,7 @@ TokenCell.prototype.render = function () {
} }
TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) { 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 const { optionsMenuActive } = this.state
return h( return h(
@ -84,9 +84,9 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) {
{ {
style: { style: {
position: 'relative', position: 'relative',
marginLeft: '-263px', marginLeft: menuToTop ? '-273px' : '-263px',
minWidth: '180px', minWidth: '180px',
marginTop: menuToTop ? '-200px' : '30px', marginTop: menuToTop ? '-214px' : '30px',
width: '280px', width: '280px',
}, },
isOpen: optionsMenuActive, isOpen: optionsMenuActive,
@ -100,6 +100,16 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) {
}, },
}, },
[ [
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
showSendTokenPage(address)
},
},
`Send`,
),
h( h(
DropdownMenuItem, DropdownMenuItem,
{ {
@ -159,3 +169,11 @@ function tokenFactoryFor (tokenAddress) {
return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
} }
const mapDispatchToProps = dispatch => {
return {
showSendTokenPage: (tokenAddress) => dispatch(actions.showSendTokenPage(tokenAddress)),
}
}
module.exports = connect(null, mapDispatchToProps)(TokenCell)

View File

@ -16,22 +16,28 @@ const Loading = require('./components/loading')
module.exports = connect(mapStateToProps)(ConfirmTxScreen) module.exports = connect(mapStateToProps)(ConfirmTxScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
const { metamask, appState } = state
const { screenParams, pendingTxIndex } = appState.currentView
return { return {
identities: state.metamask.identities, identities: metamask.identities,
accounts: state.metamask.accounts, accounts: metamask.accounts,
selectedAddress: state.metamask.selectedAddress, selectedAddress: metamask.selectedAddress,
unapprovedTxs: state.metamask.unapprovedTxs, unapprovedTxs: metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedMsgs: metamask.unapprovedMsgs,
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, unapprovedPersonalMsgs: metamask.unapprovedPersonalMsgs,
unapprovedTypedMessages: state.metamask.unapprovedTypedMessages, unapprovedTypedMessages: metamask.unapprovedTypedMessages,
index: state.appState.currentView.pendingTxIndex || 0, index: pendingTxIndex || 0,
warning: state.appState.warning, warning: appState.warning,
network: state.metamask.network, network: metamask.network,
provider: state.metamask.provider, provider: metamask.provider,
conversionRate: state.metamask.conversionRate, conversionRate: metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency, currentCurrency: metamask.currentCurrency,
blockGasLimit: state.metamask.currentBlockGasLimit, blockGasLimit: metamask.currentBlockGasLimit,
computedBalances: state.metamask.computedBalances, 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, unconfTxListLength,
computedBalances, computedBalances,
network, network,
isToken: props.isToken,
tokenSymbol: props.tokenSymbol,
tokensToSend: props.tokensToSend,
tokensTransferTo: props.tokensTransferTo,
// Actions // Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
sendTransaction: this.sendTransaction.bind(this), sendTransaction: this.sendTransaction.bind(this),

399
old-ui/app/send-token.js Normal file
View File

@ -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('')
}

View File

@ -260,7 +260,7 @@ SendTransactionScreen.prototype.onSubmit = function () {
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 nickname = state.nickname || ' ' const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value const input = document.querySelector('input[name="amount"]').value
const parts = input.split('') const parts = input.split('.')
let message let message

View File

@ -111,10 +111,11 @@ function parseBalance (balance) {
// Takes wei hex, returns an object with three properties. // Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values. // 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 isSokol = parseInt(network) === 77
const isPOA = parseInt(network) === 99 const isPOA = parseInt(network) === 99
const coinName = isPOA ? 'POA' : isSokol ? 'SPOA' : 'ETH' const coinName = isPOA ? 'POA' : isSokol ? 'SPOA' : 'ETH'
const assetName = isToken ? tokenSymbol : coinName
var parsed = needsParse ? parseBalance(balance) : balance.split('.') var parsed = needsParse ? parseBalance(balance) : balance.split('.')
var beforeDecimal = parsed[0] var beforeDecimal = parsed[0]
var afterDecimal = parsed[1] var afterDecimal = parsed[1]
@ -124,14 +125,14 @@ function formatBalance (balance, decimalsToKeep, needsParse = true, network) {
if (afterDecimal !== '0') { if (afterDecimal !== '0') {
var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits
if (sigFigs) { afterDecimal = sigFigs[0] } if (sigFigs) { afterDecimal = sigFigs[0] }
formatted = '0.' + afterDecimal + ` ${coinName}` formatted = '0.' + afterDecimal + ` ${assetName}`
} }
} else { } else {
formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${coinName}` formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${assetName}`
} }
} else { } else {
afterDecimal += Array(decimalsToKeep).join('0') afterDecimal += Array(decimalsToKeep).join('0')
formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${coinName}` formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${assetName}`
} }
return formatted return formatted
} }

View File

@ -8,9 +8,11 @@ module.exports = {
token: { token: {
menu: By.id('token-cell_dropdown_0'), menu: By.id('token-cell_dropdown_0'),
items: By.className('dropdown-menu-item'), items: By.className('dropdown-menu-item'),
view: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(2)'), send: 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)'), view: 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)'), 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', viewText: 'View token on block explorer',
copyText: 'Copy address to clipboard', copyText: 'Copy address to clipboard',
removeText: 'Remove', removeText: 'Remove',
@ -25,6 +27,7 @@ module.exports = {
}, },
account: { 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'), 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'), 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'), 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'), 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: { 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: { yourPR: {
key: By.css('#app-content > div > div.app-primary.from-right > div > div.privateKey > div.flex-row > p'), key: By.css('#app-content > div > div.app-primary.from-right > div > div.privateKey > div.flex-row > p'),
copy: By.className('clipboard cursor-pointer'), copy: By.className('clipboard cursor-pointer'),
@ -120,14 +146,17 @@ module.exports = {
titleText: 'Delete Custom RPC', titleText: 'Delete Custom RPC',
}, },
confirmTransaction: { 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'), submit: By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'),
}, },
}, },
sendTransaction: { sendTransaction: {
title: By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)'), title: By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)'),
titleText: 'Send Transaction', titleText: 'Send Transaction',
fields: { field: {
address: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'), 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'), amount: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'),
}, },
@ -179,7 +208,8 @@ module.exports = {
}, },
main: { main: {
identicon: By.className('identicon-wrapper select-none'), 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'), edit: By.className('edit-text'),
iconCopy: By.className('clipboard cursor-pointer white'), 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)'), 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: { tokens: {
menu: By.className('inactiveForm pointer'), menu: By.className('inactiveForm pointer'),
token: By.className('token-cell'), 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'), amount: By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > div > span'),
textNoTokens: 'No tokens found', textNoTokens: 'No tokens found',
textYouOwn1token: 'You own 1 token', textYouOwn1token: 'You own 1 token',

View File

@ -134,13 +134,13 @@ describe('Metamask popup page', async function () {
await menu.click() await menu.click()
const field = await waitUntilShowUp(screens.main.edit) const field = await waitUntilShowUp(screens.main.edit)
await field.click() 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.notEqual(accountName, false, '\'Account name\' change dialog isn\'t opened')
assert.equal(await accountName.getAttribute('value'), 'Account 1', 'incorrect account name') assert.equal(await accountName.getAttribute('value'), 'Account 1', 'incorrect account name')
}) })
it('fill out new account\'s name', async () => { 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.clear()
await field.sendKeys(newAccountName) 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.equal(await button.getText(), 'Save', 'button has incorrect name')
assert.notEqual(button, true, 'button \'Save\' does not present') assert.notEqual(button, true, 'button \'Save\' does not present')
await click(button) 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') 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 () { it('adds recipient address and amount', async function () {
const sendTranscationScreen = await waitUntilShowUp(screens.sendTransaction.title) const sendTranscationScreen = await waitUntilShowUp(screens.sendTransaction.title)
assert.equal(await sendTranscationScreen.getText(), screens.sendTransaction.titleText, 'Transaction screen has incorrect titlr') assert.equal(await sendTranscationScreen.getText(), screens.sendTransaction.titleText, 'Transaction screen has incorrect titlr')
const inputAddress = await waitUntilShowUp(screens.sendTransaction.fields.address) const inputAddress = await waitUntilShowUp(screens.sendTransaction.field.address)
const inputAmmount = await waitUntilShowUp(screens.sendTransaction.fields.amount) const inputAmmount = await waitUntilShowUp(screens.sendTransaction.field.amount)
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmmount.sendKeys('10') await inputAmmount.sendKeys('10')
const button = await waitUntilShowUp(screens.sendTransaction.buttonNext) const button = await waitUntilShowUp(screens.sendTransaction.buttonNext)
@ -597,7 +597,7 @@ describe('Metamask popup page', async function () {
}) })
it('confirms transaction', 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') assert.equal(await button.getAttribute('value'), 'Submit', 'button has incorrect name')
await click(button) await click(button)
}) })
@ -924,7 +924,7 @@ describe('Metamask popup page', async function () {
it('confirms transaction in MetaMask popup', async function () { it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles() const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1]) 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) await click(button)
}) })
@ -949,6 +949,9 @@ describe('Metamask popup page', async function () {
it('navigates to the add token screen', async function () { it('navigates to the add token screen', async function () {
await waitUntilShowUp(screens.main.identicon) await waitUntilShowUp(screens.main.identicon)
const tab = await waitUntilShowUp(screens.main.tokens.menu)
await tab.click()
const addTokenButton = await waitUntilShowUp(screens.main.tokens.buttonAdd) const addTokenButton = await waitUntilShowUp(screens.main.tokens.buttonAdd)
assert.equal(await addTokenButton.getText(), screens.main.tokens.buttonAddText) assert.equal(await addTokenButton.getText(), screens.main.tokens.buttonAddText)
await click(addTokenButton) await click(addTokenButton)
@ -1009,6 +1012,50 @@ describe('Metamask popup page', async function () {
await switchToFirstPage() 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 () { 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 account2 = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'
const menu = await waitUntilShowUp(menus.token.menu) const invalidAddress = '0xkqjefwblknnecwe'
await menu.click() const invalidAmount = 'eeeee'
}) const largeAmount = '123'
const preciseAmount = '0.123456789123456789123'
it('link \'View on blockexplorer...\' leads to correct page ', async function () { const negativeAmount = '-1'
const menu = await waitUntilShowUp(menus.token.view) it('switch to account 1 ', async function () {
assert.notEqual(menu, false, 'item isn\'t displayed') const accountMenu = await waitUntilShowUp(menus.account.menu)
assert.equal(await menu.getText(), menus.token.viewText, 'incorrect name') await accountMenu.click()
await menu.click() const item = await waitUntilShowUp(menus.account.account1)
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() await item.click()
menu = await waitUntilShowUp(menus.token.menu, 10) await delay(2000)
assert.notEqual(menu, false, 'menu wasn\'t closed') 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) const menu = await waitUntilShowUp(menus.token.menu)
await menu.click() await menu.click()
const item = await waitUntilShowUp(menus.token.remove) const item = await waitUntilShowUp(menus.token.send)
assert.notEqual(item, false, 'item isn\'t displayed') await item.click()
assert.equal(await item.getText(), menus.token.removeText, 'incorrect name') })
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 () { describe('Remove token , provider is localhost', function () {
it('remove option opens \'Remove token\' screen ', async function () { it('remove option opens \'Remove token\' screen ', async function () {

View File

@ -1074,7 +1074,7 @@ function sendTx (txData) {
} }
} }
function signTokenTx (tokenAddress, toAddress, amount, txData) { function signTokenTx (tokenAddress, toAddress, amount, txData, confTxScreenParams) {
return dispatch => { return dispatch => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
const token = global.eth.contract(abi).at(tokenAddress) const token = global.eth.contract(abi).at(tokenAddress)
@ -1083,7 +1083,7 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
dispatch(actions.displayWarning(err.message)) dispatch(actions.displayWarning(err.message))
}) })
dispatch(actions.showConfTxPage({})) dispatch(actions.showConfTxPage(confTxScreenParams || {}))
} }
} }
@ -1109,7 +1109,7 @@ function updateTransaction (txData) {
.then(() => updateMetamaskStateFromBackground()) .then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState))) .then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => { .then(() => {
dispatch(actions.showConfTxPage({ id: txData.id })) dispatch(actions.showConfTxPage({ id: txData.id}))
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
return txData return txData
}) })
@ -1143,6 +1143,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.clearSend()) dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id)) dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
dispatch(actions.setCurrentAccountTab('history'))
if (!hasUnconfirmedTransactions(getState())) { if (!hasUnconfirmedTransactions(getState())) {
return global.platform.closeNotificationWindow() return global.platform.closeNotificationWindow()
@ -1542,11 +1543,12 @@ function showAccountsPage () {
} }
} }
function showConfTxPage ({transForward = true, id}) { function showConfTxPage (screenParams) {
return { return {
type: actions.SHOW_CONF_TX_PAGE, type: actions.SHOW_CONF_TX_PAGE,
transForward, transForward: (screenParams.transForward || true),
id, id: screenParams.id,
value: screenParams,
} }
} }
@ -1994,9 +1996,11 @@ function showSendPage () {
} }
} }
function showSendTokenPage () {
function showSendTokenPage (address) {
return { return {
type: actions.SHOW_SEND_TOKEN_PAGE, type: actions.SHOW_SEND_TOKEN_PAGE,
value: address,
} }
} }

View File

@ -122,7 +122,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
if (isUnapproved) { if (isUnapproved) {
opts.onClick = () => { opts.onClick = () => {
this.props.showConfTxPage({ id: transactionId }) this.props.showConfTxPage({ id: transactionId})
history.push(CONFIRM_TRANSACTION_ROUTE) history.push(CONFIRM_TRANSACTION_ROUTE)
} }
opts.transactionStatus = this.context.t('notStarted') opts.transactionStatus = this.context.t('notStarted')

View File

@ -297,6 +297,7 @@ function reduceApp (state, action) {
currentView: { currentView: {
name: 'sendToken', name: 'sendToken',
context: appState.currentView.context, context: appState.currentView.context,
tokenAddress: action.value,
}, },
transForward: true, transForward: true,
warning: null, warning: null,
@ -438,6 +439,7 @@ function reduceApp (state, action) {
currentView: { currentView: {
name: 'confTx', name: 'confTx',
pendingTxIndex: action.id ? indexForPending(state, action.id) : 0, pendingTxIndex: action.id ? indexForPending(state, action.id) : 0,
screenParams: action.value,
}, },
transForward: action.transForward, transForward: action.transForward,
warning: null, warning: null,

View File

@ -47,9 +47,15 @@ function calcTokenAmount (value, decimals) {
return new BigNumber(value).div(multiplier).toNumber() 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 = { module.exports = {
tokenInfoGetter, tokenInfoGetter,
calcTokenAmount, calcTokenAmount,
calcTokenAmountWithDec,
getSymbolAndDecimals, getSymbolAndDecimals,
} }

View File

@ -54,6 +54,7 @@ async function startApp (metamaskState, accountManager, opts) {
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
const numberOfUnapprivedTx = unapprovedTxsAll.length const numberOfUnapprivedTx = unapprovedTxsAll.length
if (numberOfUnapprivedTx > 0) { if (numberOfUnapprivedTx > 0) {
store.dispatch(actions.showConfTxPage({ store.dispatch(actions.showConfTxPage({
id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id, id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id,
})) }))