Add Confirm Send token screen

This commit is contained in:
Chi Kei Chan 2017-09-22 14:34:56 -07:00
parent 13f22ff6b0
commit e1077836ce
6 changed files with 488 additions and 63 deletions

View File

@ -49,7 +49,7 @@ ConfirmSendEther.prototype.getAmount = function () {
const { conversionRate } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
console.log(txParams)
const USD = conversionUtil(txParams.value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',

View File

@ -0,0 +1,394 @@
const Component = require('react').Component
const { connect } = require('react-redux')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const abi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
const actions = require('../../actions')
const clone = require('clone')
const Identicon = require('../identicon')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
const { conversionUtil } = require('../../conversion-util')
const MIN_GAS_PRICE_GWEI_BN = new BN(1)
const GWEI_FACTOR = new BN(1e9)
const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken)
function mapStateToProps (state, ownProps) {
const { token: { symbol }, txData } = ownProps
const { txParams } = txData || {}
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const {
conversionRate,
identities,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const tokenExchangeRates = state.metamask.tokenExchangeRates
const pair = `${symbol.toLowerCase()}_eth`
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
return {
conversionRate,
identities,
selectedAddress,
tokenExchangeRate,
tokenData: tokenData || {},
}
}
function mapDispatchToProps (dispatch, ownProps) {
const { token: { symbol } } = ownProps
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)),
}
}
inherits(ConfirmSendToken, Component)
function ConfirmSendToken () {
Component.call(this)
this.state = {}
this.onSubmit = this.onSubmit.bind(this)
}
ConfirmSendToken.prototype.componentWillMount = function () {
this.props.updateTokenExchangeRate()
}
ConfirmSendToken.prototype.getAmount = function () {
const { conversionRate, tokenExchangeRate, token, tokenData } = this.props
const { params = [] } = tokenData
const { value } = params[1] || {}
const { decimals } = token
const multiplier = Math.pow(10, Number(decimals || 0))
const sendTokenAmount = Number(value / multiplier)
return {
fiat: tokenExchangeRate
? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2)
: null,
token: +sendTokenAmount.toFixed(decimals),
}
}
ConfirmSendToken.prototype.getGasFee = function () {
const { conversionRate, tokenExchangeRate, token } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { decimals } = token
// Gas
const gas = txParams.gas
const gasBn = hexToBn(gas)
// Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
const gasPriceBn = hexToBn(gasPrice)
const txFeeBn = gasBn.mul(gasPriceBn)
const USD = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: 'USD',
numberOfDecimals: 2,
conversionRate,
})
const ETH = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
return {
fiat: +Number(USD).toFixed(2),
eth: ETH,
token: tokenExchangeRate
? +(ETH * tokenExchangeRate).toFixed(decimals)
: null,
}
}
ConfirmSendToken.prototype.getData = function () {
const { identities } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
return {
from: {
address: txParams.from,
name: identities[txParams.from].name,
},
to: {
address: txParams.to,
name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient',
},
memo: txParams.memo || '',
}
}
ConfirmSendToken.prototype.renderHeroAmount = function () {
const { token: { symbol } } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { memo = '' } = txParams
return fiatAmount
? (
h('div.confirm-send-token__hero-amount-wrapper', [
h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`),
h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', memo),
]),
])
)
: (
h('div.confirm-send-token__hero-amount-wrapper', [
h('h3.flex-center.confirm-screen-send-amount', tokenAmount),
h('h3.flex-center.confirm-screen-send-amount-currency', symbol),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', memo),
]),
])
)
}
ConfirmSendToken.prototype.renderGasFee = function () {
const { token: { symbol } } = this.props
const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee()
return (
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `$${fiatGas} USD`),
h(
'div.confirm-screen-row-detail',
tokenGas ? `${tokenGas} ${symbol}` : `${ethGas} ETH`
),
]),
])
)
}
ConfirmSendToken.prototype.renderTotalPlusGas = function () {
const { token: { symbol } } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
return fiatAmount && fiatGas
? (
h('section.flex-row.flex-center.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [
h('span.confirm-screen-label', [ 'Total ' ]),
h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`),
h('div.confirm-screen-row-detail', `${tokenAmount + tokenGas} ${symbol}`),
]),
])
)
: (
h('section.flex-row.flex-center.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [
h('span.confirm-screen-label', [ 'Total ' ]),
h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
h('div.confirm-screen-row-detail', `+ ${fiatGas} USD Gas`),
]),
])
)
}
ConfirmSendToken.prototype.render = function () {
const { backToAccountDetail, selectedAddress } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const {
from: {
address: fromAddress,
name: fromName,
},
to: {
address: toAddress,
name: toName,
},
} = this.getData()
this.inputs = []
return (
h('div.flex-column.flex-grow.confirm-screen-container', {
style: { minWidth: '355px' },
}, [
// Main Send token Card
h('div.confirm-screen-wrapper.flex-column.flex-grow', [
h('h3.flex-center.confirm-screen-header', [
h('button.confirm-screen-back-button', {
onClick: () => backToAccountDetail(selectedAddress),
}, 'BACK'),
h('div.confirm-screen-title', 'Confirm Transaction'),
]),
h('div.flex-row.flex-center.confirm-screen-identicons', [
h('div.confirm-screen-account-wrapper', [
h(
Identicon,
{
address: fromAddress,
diameter: 100,
},
),
h('span.confirm-screen-account-name', fromName),
h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
]),
h('i.fa.fa-arrow-right.fa-lg'),
h('div.confirm-screen-account-wrapper', [
h(
Identicon,
{
address: txParams.to,
diameter: 100,
},
),
h('span.confirm-screen-account-name', toName),
h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
]),
]),
h('h3.flex-center.confirm-screen-sending-to-message', {
style: {
textAlign: 'center',
fontSize: '16px',
},
}, [
`You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
]),
this.renderHeroAmount(),
h('div.confirm-screen-rows', [
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', fromName),
h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', toName),
h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
]),
]),
this.renderGasFee(),
this.renderTotalPlusGas(),
]),
]),
h('form#pending-tx-form.flex-column.flex-center', {
onSubmit: this.onSubmit,
}, [
// Accept Button
h('button.confirm-screen-confirm-button', ['CONFIRM']),
// Cancel Button
h('div.cancel.btn-light.confirm-screen-cancel-button', {
onClick: (event) => this.cancel(event, txMeta),
}, 'CANCEL'),
]),
])
)
}
ConfirmSendToken.prototype.onSubmit = function (event) {
event.preventDefault()
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) {
this.props.sendTransaction(txMeta, event)
} else {
this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
this.setState({ submitting: false })
}
}
ConfirmSendToken.prototype.cancel = function (event, txMeta) {
event.preventDefault()
this.props.cancelTransaction(txMeta)
}
ConfirmSendToken.prototype.checkValidity = function () {
const form = this.getFormEl()
const valid = form.checkValidity()
return valid
}
ConfirmSendToken.prototype.getFormEl = function () {
const form = document.querySelector('form#pending-tx-form')
// Stub out form for unit tests:
if (!form) {
return { checkValidity () { return true } }
}
return form
}
// After a customizable state value has been updated,
ConfirmSendToken.prototype.gatherTxMeta = function () {
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}
ConfirmSendToken.prototype.verifyGasParams = function () {
// We call this in case the gas has not been modified at all
if (!this.state) { return true }
return (
this._notZeroOrEmptyString(this.state.gas) &&
this._notZeroOrEmptyString(this.state.gasPrice)
)
}
ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) {
return obj !== '' && obj !== '0x0'
}
ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}

View File

@ -9,6 +9,7 @@ const inherits = require('util').inherits
const actions = require('../../actions')
const util = require('../../util')
const ConfirmSendEther = require('./confirm-send-ether')
const ConfirmSendToken = require('./confirm-send-token')
const TX_TYPES = {
DEPLOY_CONTRACT: 'deploy_contract',
@ -46,33 +47,51 @@ function PendingTx () {
this.state = {
isFetching: true,
transactionType: '',
tokenAddress: '',
tokenSymbol: '',
tokenDecimals: '',
}
}
PendingTx.prototype.componentWillMount = function () {
PendingTx.prototype.componentWillMount = async function () {
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
this.props.setCurrentCurrencyToUSD()
if (txParams.to) {
if (!txParams.to) {
return this.setState({
transactionType: TX_TYPES.DEPLOY_CONTRACT,
isFetching: false,
})
}
try {
const token = util.getContractAtAddress(txParams.to)
token
.symbol()
.then(result => {
const symbol = result[0] || null
const results = await Promise.all([
token.symbol(),
token.decimals(),
])
const [ symbol, decimals ] = results
if (symbol[0] && decimals[0]) {
this.setState({
transactionType: symbol ? TX_TYPES.SEND_TOKEN : TX_TYPES.SEND_ETHER,
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: symbol[0],
tokenDecimals: decimals[0],
isFetching: false,
})
})
.catch(() => this.setState({
transactionType: TX_TYPES.SEND_ETHER,
isFetching: false,
}))
} else {
} else {
this.setState({
transactionType: TX_TYPES.SEND_ETHER,
isFetching: false,
})
}
} catch (e) {
this.setState({
transactionType: TX_TYPES.DEPLOY_CONTRACT,
transactionType: TX_TYPES.SEND_ETHER,
isFetching: false,
})
}
@ -87,16 +106,36 @@ PendingTx.prototype.gatherTxMeta = function () {
}
PendingTx.prototype.render = function () {
const { isFetching, transactionType } = this.state
const {
isFetching,
transactionType,
tokenAddress,
tokenSymbol,
tokenDecimals,
} = this.state
const { sendTransaction } = this.props
if (isFetching) {
return h('noscript')
}
switch (transactionType) {
case TX_TYPES.SEND_ETHER:
return h(ConfirmSendEther, { txData: this.gatherTxMeta() })
return h(ConfirmSendEther, {
txData: this.gatherTxMeta(),
sendTransaction,
})
case TX_TYPES.SEND_TOKEN:
return h(ConfirmSendToken, {
txData: this.gatherTxMeta(),
sendTransaction,
token: {
address: tokenAddress,
symbol: tokenSymbol,
decimals: tokenDecimals,
},
})
default:
return h('noscript')
}

View File

@ -1,7 +1,6 @@
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const { addHexPrefix } = require('ethereumjs-util')
const classnames = require('classnames')
const inherits = require('util').inherits
const actions = require('../../actions')
@ -26,20 +25,15 @@ function mapStateToProps (state) {
const conversionRate = state.metamask.conversionRate
const currentBlockGasLimit = state.metamask.currentBlockGasLimit
const accounts = state.metamask.accounts
// const network = state.metamask.network
const selectedTokenAddress = state.metamask.selectedTokenAddress
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const selectedToken = selectors.getSelectedToken(state)
const tokenExchangeRates = state.metamask.tokenExchangeRates
const pair = `${selectedToken.symbol.toLowerCase()}_eth`
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
// const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress)
// const identity = identities[selectedAddress]
return {
// sidebarOpen,
selectedAddress,
// checksumAddress,
selectedTokenAddress,
identities,
addressBook,
@ -48,9 +42,6 @@ function mapStateToProps (state) {
currentBlockGasLimit,
selectedToken,
warning,
// selectedToken: selectors.getSelectedToken(state),
// identity,
// network,
}
}
@ -66,11 +57,6 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
),
updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
// showSidebar: () => { dispatch(actions.showSidebar()) },
// hideSidebar: () => { dispatch(actions.hideSidebar()) },
// showModal: (payload) => { dispatch(actions.showModal(payload)) },
// showSendPage: () => { dispatch(actions.showSendPage()) },
// showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
}
}
@ -116,7 +102,7 @@ SendTokenScreen.prototype.validate = function () {
gasLimit: !gasLimit ? 'Gas Limit Required' : null,
}
if(to && !isValidAddress(to)) {
if (to && !isValidAddress(to)) {
errors.to = 'Invalid address'
}
@ -360,7 +346,7 @@ SendTokenScreen.prototype.render = function () {
this.renderAmountInput(),
this.renderGasInput(),
this.renderMemoInput(),
warning && h('div.send-screen-input-wrapper--error', {},
warning && h('div.send-screen-input-wrapper--error',
h('div.send-screen-input-wrapper__error-message', [
warning,
])

View File

@ -387,3 +387,9 @@
}
}
}
.confirm-send-token {
&__hero-amount-wrapper {
width: 100%;
}
}

View File

@ -380,36 +380,36 @@ function reduceApp (state, action) {
case actions.COMPLETED_TX:
log.debug('reducing COMPLETED_TX for tx ' + action.value)
const otherUnconfActions = getUnconfActionList(state)
.filter(tx => tx.id !== action.value)
const hasOtherUnconfActions = otherUnconfActions.length > 0
// const otherUnconfActions = getUnconfActionList(state)
// .filter(tx => tx.id !== action.value)
// const hasOtherUnconfActions = otherUnconfActions.length > 0
if (hasOtherUnconfActions) {
log.debug('reducer detected txs - rendering confTx view')
return extend(appState, {
transForward: false,
currentView: {
name: 'confTx',
context: 0,
},
warning: null,
})
} else {
log.debug('attempting to close popup')
return extend(appState, {
// indicate notification should close
shouldClose: true,
transForward: false,
warning: null,
currentView: {
name: 'accountDetail',
context: state.metamask.selectedAddress,
},
accountDetail: {
subview: 'transactions',
},
})
}
// if (hasOtherUnconfActions) {
// log.debug('reducer detected txs - rendering confTx view')
// return extend(appState, {
// transForward: false,
// currentView: {
// name: 'confTx',
// context: 0,
// },
// warning: null,
// })
// } else {
log.debug('attempting to close popup')
return extend(appState, {
// indicate notification should close
shouldClose: true,
transForward: false,
warning: null,
currentView: {
name: 'accountDetail',
context: state.metamask.selectedAddress,
},
accountDetail: {
subview: 'transactions',
},
})
// }
case actions.NEXT_TX:
return extend(appState, {