aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components
diff options
context:
space:
mode:
authorChi Kei Chan <chikeichan@gmail.com>2017-09-23 05:34:56 +0800
committerChi Kei Chan <chikeichan@gmail.com>2017-09-23 05:38:17 +0800
commite1077836ce916e2bd788451e3f365324024a1c0c (patch)
tree77954f105cc7c693a57ac8790c928286bed5d384 /ui/app/components
parent13f22ff6b087f3865f84a0672a9013ada88be61a (diff)
downloadtangerine-wallet-browser-e1077836ce916e2bd788451e3f365324024a1c0c.tar.gz
tangerine-wallet-browser-e1077836ce916e2bd788451e3f365324024a1c0c.tar.zst
tangerine-wallet-browser-e1077836ce916e2bd788451e3f365324024a1c0c.zip
Add Confirm Send token screen
Diffstat (limited to 'ui/app/components')
-rw-r--r--ui/app/components/pending-tx/confirm-send-ether.js2
-rw-r--r--ui/app/components/pending-tx/confirm-send-token.js394
-rw-r--r--ui/app/components/pending-tx/index.js73
-rw-r--r--ui/app/components/send-token/index.js18
4 files changed, 453 insertions, 34 deletions
diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js
index 29c6d349c..b03ec0552 100644
--- a/ui/app/components/pending-tx/confirm-send-ether.js
+++ b/ui/app/components/pending-tx/confirm-send-ether.js
@@ -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',
diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js
new file mode 100644
index 000000000..384ac92cc
--- /dev/null
+++ b/ui/app/components/pending-tx/confirm-send-token.js
@@ -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)
+}
diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js
index 3797b5642..915319958 100644
--- a/ui/app/components/pending-tx/index.js
+++ b/ui/app/components/pending-tx/index.js
@@ -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')
}
diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js
index 7adbf48dc..dd8ca6b9d 100644
--- a/ui/app/components/send-token/index.js
+++ b/ui/app/components/send-token/index.js
@@ -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,
])