"Send all" coins option
This commit is contained in:
parent
93e48e204e
commit
de6ab01e4a
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Current Master
|
||||
|
||||
- [#388](https://github.com/poanetwork/nifty-wallet/pull/388) - (Feature) "Send all" option for simple coin transfers
|
||||
- [#385](https://github.com/poanetwork/nifty-wallet/pull/385) - (Feature) Display value of current pending tx's nonce on send tx screen
|
||||
- [#384](https://github.com/poanetwork/nifty-wallet/pull/384) - (Fix) placement of HW Connect button title
|
||||
- [#383](https://github.com/poanetwork/nifty-wallet/pull/383) - (Chore) Replace POA-ETH Binance link to POA-BTC
|
||||
|
|
|
@ -10,7 +10,7 @@ import abiDecoder from 'abi-decoder'
|
|||
abiDecoder.addABI(abi)
|
||||
|
||||
import TransactionStateManager from './tx-state-manager'
|
||||
const TxGasUtil = require('./tx-gas-utils')
|
||||
import TxGasUtil from './tx-gas-utils'
|
||||
const PendingTransactionTracker = require('./pending-tx-tracker')
|
||||
import NonceTracker from 'nonce-tracker'
|
||||
import * as txUtils from './lib/util'
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
const EthQuery = require('ethjs-query')
|
||||
const {
|
||||
hexToBn,
|
||||
BnMultiplyByFraction,
|
||||
bnToHex,
|
||||
} = require('../../lib/util')
|
||||
const { addHexPrefix } = require('ethereumjs-util')
|
||||
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
|
||||
import EthQuery from 'ethjs-query'
|
||||
import { hexToBn, BnMultiplyByFraction, bnToHex } from '../../lib/util'
|
||||
import { addHexPrefix } from 'ethereumjs-util'
|
||||
import { MIN_GAS_LIMIT_HEX } from '../../../../ui/app/components/send/send.constants'
|
||||
import log from 'loglevel'
|
||||
|
||||
/**
|
||||
tx-gas-utils are gas utility methods for Transaction manager
|
||||
|
@ -14,24 +11,31 @@ and used to do things like calculate gas of a tx.
|
|||
@param {Object} provider - A network provider.
|
||||
*/
|
||||
|
||||
class TxGasUtil {
|
||||
export default class TxGasUtil {
|
||||
|
||||
constructor (provider) {
|
||||
this.query = new EthQuery(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
@param txMeta {Object} - the txMeta object
|
||||
@returns {object} the txMeta object with the gas written to the txParams
|
||||
@param {Object} txMeta - the txMeta object
|
||||
@returns {GasAnalysisResult} The result of the gas analysis
|
||||
*/
|
||||
async analyzeGasUsage (txMeta) {
|
||||
const block = await this.query.getBlockByNumber('latest', false)
|
||||
let estimatedGasHex
|
||||
|
||||
// fallback to block gasLimit
|
||||
const blockGasLimitBN = hexToBn(block.gasLimit)
|
||||
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
|
||||
let estimatedGasHex = bnToHex(saferGasLimitBN)
|
||||
try {
|
||||
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
log.warn(error)
|
||||
txMeta.simulationFails = {
|
||||
reason: err.message,
|
||||
reason: error.message,
|
||||
errorKey: error.errorKey,
|
||||
debug: { blockNumber: block.number, blockGasLimit: block.gasLimit },
|
||||
}
|
||||
return txMeta
|
||||
}
|
||||
|
@ -63,9 +67,9 @@ class TxGasUtil {
|
|||
if (recipient) code = await this.query.getCode(recipient)
|
||||
|
||||
if (hasRecipient && (!code || code === '0x' || code === '0x0')) {
|
||||
txParams.gas = SIMPLE_GAS_COST
|
||||
txParams.gas = MIN_GAS_LIMIT_HEX
|
||||
txMeta.simpleSend = true // Prevents buffer addition
|
||||
return SIMPLE_GAS_COST
|
||||
return MIN_GAS_LIMIT_HEX
|
||||
}
|
||||
|
||||
// if not, fall back to block gasLimit
|
||||
|
@ -103,9 +107,9 @@ class TxGasUtil {
|
|||
/**
|
||||
Adds a gas buffer with out exceeding the block gas limit
|
||||
|
||||
@param initialGasLimitHex {string} - the initial gas limit to add the buffer too
|
||||
@param blockGasLimitHex {string} - the block gas limit
|
||||
@returns {string} the buffered gas limit as a hex string
|
||||
@param {string} initialGasLimitHex - the initial gas limit to add the buffer too
|
||||
@param {string} blockGasLimitHex - the block gas limit
|
||||
@returns {string} - the buffered gas limit as a hex string
|
||||
*/
|
||||
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
||||
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
||||
|
@ -114,12 +118,14 @@ class TxGasUtil {
|
|||
const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
|
||||
|
||||
// if initialGasLimit is above blockGasLimit, dont modify it
|
||||
if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn)
|
||||
if (initialGasLimitBn.gt(upperGasLimitBn)) {
|
||||
return bnToHex(initialGasLimitBn)
|
||||
}
|
||||
// if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
|
||||
if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
|
||||
if (bufferedGasLimitBn.lt(upperGasLimitBn)) {
|
||||
return bnToHex(bufferedGasLimitBn)
|
||||
}
|
||||
// otherwise use blockGasLimit
|
||||
return bnToHex(upperGasLimitBn)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TxGasUtil
|
||||
|
|
|
@ -443,7 +443,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
setDProvider: this.setDProvider.bind(this),
|
||||
markPasswordForgotten: this.markPasswordForgotten.bind(this),
|
||||
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
|
||||
getGasPrice: (cb) => cb(null, this.getGasPrice()),
|
||||
getGasPrice: nodeify(this.getGasPrice, this),
|
||||
getPendingNonce: nodeify(this.getPendingNonce, this),
|
||||
|
||||
// shapeshift
|
||||
|
@ -1870,10 +1870,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @returns {string} A hex representation of the suggested wei gas price.
|
||||
*/
|
||||
async getGasPrice () {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const { networkController } = this
|
||||
|
||||
|
||||
const networkIdStr = networkController.store.getState().network
|
||||
const networkId = parseInt(networkIdStr)
|
||||
const isETHC = networkId === CLASSIC_CODE || networkId === MAINNET_CODE
|
||||
|
|
|
@ -34,7 +34,8 @@ class ConfirmSeedScreen extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
const { seedWords, history } = this.props
|
||||
|
||||
if (!seedWords) {
|
||||
|
|
|
@ -40,7 +40,8 @@ class CreatePasswordScreen extends Component {
|
|||
this.animationEventEmitter = new EventEmitter()
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
const { isInitialized, history } = this.props
|
||||
|
||||
if (isInitialized) {
|
||||
|
|
|
@ -58,7 +58,8 @@ class BackupPhraseScreen extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
const { seedWords, history } = this.props
|
||||
|
||||
if (!seedWords) {
|
||||
|
|
|
@ -26,7 +26,8 @@ export class ShapeShiftForm extends Component {
|
|||
isLoading: false,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.shapeShiftSubview()
|
||||
}
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ AddSuggestedTokenScreen.prototype.render = function () {
|
|||
)
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
|
||||
AddSuggestedTokenScreen.prototype.UNSAFE_componentWillMount = function () {
|
||||
if (typeof global.ethereumProvider === 'undefined') return
|
||||
}
|
||||
|
||||
|
|
|
@ -316,7 +316,8 @@ export default class AddTokenScreen extends Component {
|
|||
])
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
if (typeof global.ethereumProvider === 'undefined') return
|
||||
|
||||
this.eth = new Eth(global.ethereumProvider)
|
||||
|
|
|
@ -1,200 +1,209 @@
|
|||
const Component = require('react').Component
|
||||
import { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import debounce from 'debounce'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import ENS from 'ethjs-ens'
|
||||
import log from 'loglevel'
|
||||
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const debounce = require('debounce')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const ENS = require('ethjs-ens')
|
||||
const networkMap = require('ethjs-ens/lib/network-map.json')
|
||||
const ensRE = /.+\..+$/
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
const log = require('loglevel')
|
||||
const { isValidENSAddress } = require('../util')
|
||||
|
||||
|
||||
module.exports = EnsInput
|
||||
class EnsInput extends Component {
|
||||
static propTypes = {
|
||||
network: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
identities: PropTypes.object,
|
||||
addressBook: PropTypes.array,
|
||||
updateSendTo: PropTypes.func,
|
||||
}
|
||||
|
||||
inherits(EnsInput, Component)
|
||||
function EnsInput () {
|
||||
Component.call(this)
|
||||
}
|
||||
render () {
|
||||
const props = this.props
|
||||
|
||||
EnsInput.prototype.render = function () {
|
||||
const props = this.props
|
||||
function onInputChange () {
|
||||
const recipient = document.querySelector('input[name="address"]').value
|
||||
this.props.updateSendTo(recipient, ' ')
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
if (!networkHasEnsSupport) return
|
||||
|
||||
function onInputChange () {
|
||||
if (recipient.match(ensRE) === null) {
|
||||
return this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: null,
|
||||
ensFailure: null,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingEns: true,
|
||||
})
|
||||
this.checkName()
|
||||
}
|
||||
|
||||
return (
|
||||
h('div', {
|
||||
style: { width: '100%' },
|
||||
}, [
|
||||
h('input.large-input', {
|
||||
name: props.name,
|
||||
placeholder: props.placeholder,
|
||||
list: 'addresses',
|
||||
onChange: onInputChange.bind(this),
|
||||
}),
|
||||
// The address book functionality.
|
||||
h('datalist#addresses',
|
||||
[
|
||||
// Corresponds to the addresses owned.
|
||||
Object.keys(props.identities).map((key) => {
|
||||
const identity = props.identities[key]
|
||||
return h('option', {
|
||||
value: identity.address,
|
||||
label: identity.name,
|
||||
key: identity.address,
|
||||
})
|
||||
}),
|
||||
// Corresponds to previously sent-to addresses.
|
||||
props.addressBook.map((identity) => {
|
||||
return h('option', {
|
||||
value: identity.address,
|
||||
label: identity.name,
|
||||
key: identity.address,
|
||||
})
|
||||
}),
|
||||
]),
|
||||
this.ensIcon(),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
if (!networkHasEnsSupport) return
|
||||
this.setState({ ensResolution: ZERO_ADDRESS })
|
||||
|
||||
if (networkHasEnsSupport) {
|
||||
const provider = global.ethereumProvider
|
||||
this.ens = new ENS({ provider, network })
|
||||
this.checkName = debounce(this.lookupEnsName.bind(this), 200)
|
||||
}
|
||||
}
|
||||
|
||||
lookupEnsName () {
|
||||
const recipient = document.querySelector('input[name="address"]').value
|
||||
if (recipient.match(ensRE) === null) {
|
||||
return this.setState({
|
||||
const { ensResolution } = this.state
|
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`)
|
||||
this.ens.lookup(recipient.trim())
|
||||
.then((address) => {
|
||||
if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.')
|
||||
if (address !== ensResolution) {
|
||||
this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: address,
|
||||
nickname: recipient.trim(),
|
||||
hoverText: address + '\nClick to Copy',
|
||||
ensFailure: false,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
const setStateObj = {
|
||||
loadingEns: false,
|
||||
ensResolution: null,
|
||||
ensFailure: null,
|
||||
ensResolution: recipient,
|
||||
ensFailure: true,
|
||||
toError: null,
|
||||
}
|
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
|
||||
setStateObj.hoverText = 'ENS name not found'
|
||||
setStateObj.toError = 'ensNameNotFound'
|
||||
setStateObj.ensFailure = false
|
||||
} else {
|
||||
log.error(reason)
|
||||
setStateObj.hoverText = reason.message
|
||||
}
|
||||
|
||||
return this.setState(setStateObj)
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
const state = this.state || {}
|
||||
const ensResolution = state.ensResolution
|
||||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
const nickname = state.nickname || ' '
|
||||
if (prevState && ensResolution && this.props.onChange &&
|
||||
ensResolution !== prevState.ensResolution) {
|
||||
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
|
||||
}
|
||||
}
|
||||
|
||||
ensIcon () {
|
||||
const { hoverText } = this.state || {}
|
||||
return h('span', {
|
||||
title: hoverText,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
padding: '6px 0px',
|
||||
right: '0px',
|
||||
transform: 'translatex(-40px)',
|
||||
},
|
||||
}, this.ensIconContents())
|
||||
}
|
||||
|
||||
ensIconContents () {
|
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS}
|
||||
|
||||
if (toError) return
|
||||
|
||||
if (loadingEns) {
|
||||
return h('img', {
|
||||
src: 'images/loading.svg',
|
||||
style: {
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
transform: 'translateY(-6px)',
|
||||
marginRight: '-5px',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingEns: true,
|
||||
})
|
||||
this.checkName()
|
||||
}
|
||||
|
||||
return (
|
||||
h('div', {
|
||||
style: { width: '100%' },
|
||||
}, [
|
||||
h('input.large-input', {
|
||||
name: props.name,
|
||||
placeholder: props.placeholder,
|
||||
list: 'addresses',
|
||||
onChange: onInputChange.bind(this),
|
||||
}),
|
||||
// The address book functionality.
|
||||
h('datalist#addresses',
|
||||
[
|
||||
// Corresponds to the addresses owned.
|
||||
Object.keys(props.identities).map((key) => {
|
||||
const identity = props.identities[key]
|
||||
return h('option', {
|
||||
value: identity.address,
|
||||
label: identity.name,
|
||||
key: identity.address,
|
||||
})
|
||||
}),
|
||||
// Corresponds to previously sent-to addresses.
|
||||
props.addressBook.map((identity) => {
|
||||
return h('option', {
|
||||
value: identity.address,
|
||||
label: identity.name,
|
||||
key: identity.address,
|
||||
})
|
||||
}),
|
||||
]),
|
||||
this.ensIcon(),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
EnsInput.prototype.componentDidMount = function () {
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
this.setState({ ensResolution: ZERO_ADDRESS })
|
||||
|
||||
if (networkHasEnsSupport) {
|
||||
const provider = global.ethereumProvider
|
||||
this.ens = new ENS({ provider, network })
|
||||
this.checkName = debounce(this.lookupEnsName.bind(this), 200)
|
||||
}
|
||||
}
|
||||
|
||||
EnsInput.prototype.lookupEnsName = function () {
|
||||
const recipient = document.querySelector('input[name="address"]').value
|
||||
const { ensResolution } = this.state
|
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`)
|
||||
this.ens.lookup(recipient.trim())
|
||||
.then((address) => {
|
||||
if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.')
|
||||
if (address !== ensResolution) {
|
||||
this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: address,
|
||||
nickname: recipient.trim(),
|
||||
hoverText: address + '\nClick to Copy',
|
||||
ensFailure: false,
|
||||
toError: null,
|
||||
if (ensFailure) {
|
||||
return h('i.fa.fa-warning.fa-lg.warning', {
|
||||
style: {
|
||||
color: '#df2265',
|
||||
background: 'white',
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
const setStateObj = {
|
||||
loadingEns: false,
|
||||
ensResolution: recipient,
|
||||
ensFailure: true,
|
||||
toError: null,
|
||||
|
||||
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
|
||||
return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
|
||||
style: {
|
||||
color: '#60db97',
|
||||
background: 'white',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copyToClipboard(ensResolution)
|
||||
},
|
||||
})
|
||||
}
|
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
|
||||
setStateObj.hoverText = 'ENS name not found'
|
||||
setStateObj.toError = 'ensNameNotFound'
|
||||
setStateObj.ensFailure = false
|
||||
} else {
|
||||
log.error(reason)
|
||||
setStateObj.hoverText = reason.message
|
||||
}
|
||||
|
||||
return this.setState(setStateObj)
|
||||
})
|
||||
}
|
||||
|
||||
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
|
||||
const state = this.state || {}
|
||||
const ensResolution = state.ensResolution
|
||||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
const nickname = state.nickname || ' '
|
||||
if (prevState && ensResolution && this.props.onChange &&
|
||||
ensResolution !== prevState.ensResolution) {
|
||||
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
|
||||
}
|
||||
}
|
||||
|
||||
EnsInput.prototype.ensIcon = function (recipient) {
|
||||
const { hoverText } = this.state || {}
|
||||
return h('span', {
|
||||
title: hoverText,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
padding: '6px 0px',
|
||||
right: '0px',
|
||||
transform: 'translatex(-40px)',
|
||||
},
|
||||
}, this.ensIconContents(recipient))
|
||||
}
|
||||
|
||||
EnsInput.prototype.ensIconContents = function (recipient) {
|
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS}
|
||||
|
||||
if (toError) return
|
||||
|
||||
if (loadingEns) {
|
||||
return h('img', {
|
||||
src: 'images/loading.svg',
|
||||
style: {
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
transform: 'translateY(-6px)',
|
||||
marginRight: '-5px',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (ensFailure) {
|
||||
return h('i.fa.fa-warning.fa-lg.warning', {
|
||||
style: {
|
||||
color: '#df2265',
|
||||
background: 'white',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
|
||||
return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
|
||||
style: {
|
||||
color: '#60db97',
|
||||
background: 'white',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copyToClipboard(ensResolution)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getNetworkEnsSupport (network) {
|
||||
return Boolean(networkMap[network])
|
||||
}
|
||||
|
||||
module.exports = EnsInput
|
||||
|
|
|
@ -25,10 +25,11 @@ const { tokenInfoGetter, calcTokenAmount } = require('../../../ui/app/token-util
|
|||
import BigNumber from 'bignumber.js'
|
||||
import ethNetProps from 'eth-net-props'
|
||||
import { getMetaMaskAccounts } from '../../../ui/app/selectors'
|
||||
import { MIN_GAS_LIMIT_DEC } from '../../../ui/app/components/send/send.constants'
|
||||
import * as Toast from './toast'
|
||||
|
||||
const MIN_GAS_PRICE_BN = new BN('0')
|
||||
const MIN_GAS_LIMIT_BN = new BN('21000')
|
||||
const MIN_GAS_LIMIT_BN = new BN(MIN_GAS_LIMIT_DEC)
|
||||
const emptyAddress = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
class PendingTx extends Component {
|
||||
|
@ -472,12 +473,7 @@ class PendingTx extends Component {
|
|||
},
|
||||
}, [
|
||||
h('.cell.label'),
|
||||
h('.cell.value', {
|
||||
style: {
|
||||
fontFamily: 'Nunito Regular',
|
||||
fontSize: '14px',
|
||||
},
|
||||
}, `Data included: ${dataLength} bytes`),
|
||||
h('.cell.value', `Data included: ${dataLength} bytes`),
|
||||
]),
|
||||
]), // End of Table
|
||||
|
||||
|
@ -591,7 +587,8 @@ class PendingTx extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
const txMeta = this.gatherTxMeta()
|
||||
const txParams = txMeta.txParams || {}
|
||||
if (this.props.isToken || this.state.isToken) {
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class AmountMaxButton extends Component {
|
||||
|
||||
static propTypes = {
|
||||
balance: PropTypes.string,
|
||||
buttonDataLoading: PropTypes.bool,
|
||||
clearMaxAmount: PropTypes.func,
|
||||
inError: PropTypes.bool,
|
||||
gasTotal: PropTypes.string,
|
||||
maxModeOn: PropTypes.bool,
|
||||
sendToken: PropTypes.object,
|
||||
setAmountToMax: PropTypes.func,
|
||||
setMaxModeTo: PropTypes.func,
|
||||
updateGasData: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
address: PropTypes.string,
|
||||
amount: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
blockGasLimit: PropTypes.string,
|
||||
data: PropTypes.string,
|
||||
}
|
||||
|
||||
async setMaxAmount () {
|
||||
const {
|
||||
updateGasData,
|
||||
address,
|
||||
sendToken,
|
||||
amount: value,
|
||||
to,
|
||||
data,
|
||||
blockGasLimit,
|
||||
setAmountToMax,
|
||||
} = this.props
|
||||
const params = { address, sendToken, blockGasLimit, to, value, data }
|
||||
await updateGasData(params)
|
||||
|
||||
const {
|
||||
balance,
|
||||
gasTotal,
|
||||
tokenBalance,
|
||||
} = this.props
|
||||
|
||||
setAmountToMax({
|
||||
balance,
|
||||
gasTotal,
|
||||
sendToken,
|
||||
tokenBalance,
|
||||
})
|
||||
}
|
||||
|
||||
onMaxClick = () => {
|
||||
const { setMaxModeTo, clearMaxAmount, maxModeOn } = this.props
|
||||
|
||||
if (!maxModeOn) {
|
||||
setMaxModeTo(true)
|
||||
this.setMaxAmount()
|
||||
} else {
|
||||
setMaxModeTo(false)
|
||||
clearMaxAmount()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { maxModeOn, buttonDataLoading, inError } = this.props
|
||||
|
||||
return (
|
||||
<div className="send__amount-max secondary-description" onClick={buttonDataLoading || inError ? null : this.onMaxClick}>
|
||||
<input type="checkbox" checked={maxModeOn} readOnly />
|
||||
{'send max amount'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getGasTotal,
|
||||
getSendToken,
|
||||
getSendFromBalance,
|
||||
getTokenBalance,
|
||||
getSendMaxModeState,
|
||||
getSendTo,
|
||||
getSendHexData,
|
||||
} from '../../../../../ui/app/selectors'
|
||||
import { calcMaxAmount } from './amount-max-button.utils.js'
|
||||
import {
|
||||
updateSendAmount,
|
||||
setMaxModeTo,
|
||||
updateGasData,
|
||||
} from '../../../../../ui/app/actions'
|
||||
import AmountMaxButton from './amount-max-button.component'
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
|
||||
return {
|
||||
balance: getSendFromBalance(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
maxModeOn: getSendMaxModeState(state),
|
||||
sendToken: getSendToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
send: state.metamask.send,
|
||||
amount: state.metamask.send.amount,
|
||||
blockGasLimit: state.metamask.currentBlockGasLimit,
|
||||
address: state.metamask.selectedAddress,
|
||||
to: getSendTo(state),
|
||||
data: getSendHexData(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
setAmountToMax: (maxAmountDataObject) => {
|
||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
|
||||
},
|
||||
clearMaxAmount: () => {
|
||||
dispatch(updateSendAmount('0'))
|
||||
},
|
||||
setMaxModeTo: (bool) => dispatch(setMaxModeTo(bool)),
|
||||
updateGasData: (params) => dispatch(updateGasData(params)),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { multiplyCurrencies, subtractCurrencies, BIG_NUMBER_WEI_MULTIPLIER } from '../../../../../ui/app/conversion-util'
|
||||
import ethUtil from 'ethereumjs-util'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
export function calcMaxAmount ({ balance, gasTotal, sendToken, tokenBalance }) {
|
||||
const { decimals } = sendToken || {}
|
||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||
|
||||
|
||||
let maxBalance
|
||||
if (sendToken) {
|
||||
maxBalance = multiplyCurrencies(
|
||||
tokenBalance,
|
||||
multiplier,
|
||||
{
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
const maxBalanceInWei =
|
||||
subtractCurrencies(
|
||||
ethUtil.addHexPrefix(balance),
|
||||
ethUtil.addHexPrefix(gasTotal),
|
||||
{ toNumericBase: 'dec' },
|
||||
)
|
||||
const maxBalanceInWeiBN = new BigNumber(maxBalanceInWei.toString())
|
||||
maxBalance = maxBalanceInWeiBN.div(BIG_NUMBER_WEI_MULTIPLIER).toString()
|
||||
}
|
||||
|
||||
return maxBalance
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default } from './amount-max-button.container'
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import AmountMaxButton from '../amount-max-button.component.js'
|
||||
|
||||
describe('AmountMaxButton Component', function () {
|
||||
let wrapper
|
||||
let instance
|
||||
|
||||
const propsMethodSpies = {
|
||||
setAmountToMax: sinon.spy(),
|
||||
setMaxModeTo: sinon.spy(),
|
||||
}
|
||||
|
||||
const MOCK_EVENT = { preventDefault: () => {} }
|
||||
|
||||
before(function () {
|
||||
sinon.spy(AmountMaxButton.prototype, 'setMaxAmount')
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
wrapper = shallow((
|
||||
<AmountMaxButton
|
||||
balance="mockBalance"
|
||||
gasTotal="mockGasTotal"
|
||||
maxModeOn={false}
|
||||
sendToken={ { address: 'mockTokenAddress' } }
|
||||
setAmountToMax={propsMethodSpies.setAmountToMax}
|
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo}
|
||||
tokenBalance="mockTokenBalance"
|
||||
/>
|
||||
), {
|
||||
context: {
|
||||
t: (str) => str + '_t',
|
||||
metricsEvent: () => {},
|
||||
},
|
||||
})
|
||||
instance = wrapper.instance()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
propsMethodSpies.setAmountToMax.resetHistory()
|
||||
propsMethodSpies.setMaxModeTo.resetHistory()
|
||||
AmountMaxButton.prototype.setMaxAmount.resetHistory()
|
||||
})
|
||||
|
||||
after(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe('setMaxAmount', function () {
|
||||
|
||||
it('should call setAmountToMax with the correct params', function () {
|
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 0)
|
||||
instance.setMaxAmount()
|
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setAmountToMax.getCall(0).args,
|
||||
[{
|
||||
balance: 'mockBalance',
|
||||
gasTotal: 'mockGasTotal',
|
||||
sendToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
}],
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('render', function () {
|
||||
it('should render an element with a send-v2__amount-max class', function () {
|
||||
assert(wrapper.exists('.send-v2__amount-max'))
|
||||
})
|
||||
|
||||
it('should call setMaxModeTo and setMaxAmount when the checkbox is checked', function () {
|
||||
const {
|
||||
onClick,
|
||||
} = wrapper.find('.send-v2__amount-max').props()
|
||||
|
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0)
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
|
||||
onClick(MOCK_EVENT)
|
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1)
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setMaxModeTo.getCall(0).args,
|
||||
[true],
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the expected text when maxModeOn is false', function () {
|
||||
wrapper.setProps({ maxModeOn: false })
|
||||
assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,93 @@
|
|||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
const actionSpies = {
|
||||
setMaxModeTo: sinon.spy(),
|
||||
updateSendAmount: sinon.spy(),
|
||||
}
|
||||
const duckActionSpies = {
|
||||
updateSendErrors: sinon.spy(),
|
||||
}
|
||||
|
||||
proxyquire('../amount-max-button.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../../../../../selectors': {
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
getSendToken: (s) => `mockSendToken:${s}`,
|
||||
getSendFromBalance: (s) => `mockBalance:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
getSendMaxModeState: (s) => `mockMaxModeOn:${s}`,
|
||||
getBasicGasEstimateLoadingStatus: (s) => `mockButtonDataLoading:${s}`,
|
||||
},
|
||||
'./amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
|
||||
'../../../../../store/actions': actionSpies,
|
||||
'../../../../../ducks/send/send.duck': duckActionSpies,
|
||||
})
|
||||
|
||||
describe('amount-max-button container', function () {
|
||||
|
||||
describe('mapStateToProps()', function () {
|
||||
|
||||
it('should map the correct properties to props', function () {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
balance: 'mockBalance:mockState',
|
||||
buttonDataLoading: 'mockButtonDataLoading:mockState',
|
||||
gasTotal: 'mockGasTotal:mockState',
|
||||
maxModeOn: 'mockMaxModeOn:mockState',
|
||||
sendToken: 'mockSendToken:mockState',
|
||||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', function () {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(function () {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
describe('setAmountToMax()', function () {
|
||||
it('should dispatch an action', function () {
|
||||
mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' })
|
||||
assert(dispatchSpy.calledTwice)
|
||||
assert(duckActionSpies.updateSendErrors.calledOnce)
|
||||
assert.deepEqual(
|
||||
duckActionSpies.updateSendErrors.getCall(0).args[0],
|
||||
{ amount: null },
|
||||
)
|
||||
assert(actionSpies.updateSendAmount.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.updateSendAmount.getCall(0).args[0],
|
||||
12,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMaxModeTo()', function () {
|
||||
it('should dispatch an action', function () {
|
||||
mapDispatchToPropsObject.setMaxModeTo('mockVal')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.setMaxModeTo.getCall(0).args[0],
|
||||
'mockVal',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,27 @@
|
|||
import assert from 'assert'
|
||||
import {
|
||||
calcMaxAmount,
|
||||
} from '../amount-max-button.utils.js'
|
||||
|
||||
describe('amount-max-button utils', function () {
|
||||
|
||||
describe('calcMaxAmount()', function () {
|
||||
it('should calculate the correct amount when no sendToken defined', function () {
|
||||
assert.deepEqual(calcMaxAmount({
|
||||
balance: 'ffffff',
|
||||
gasTotal: 'ff',
|
||||
sendToken: false,
|
||||
}), 'ffff00')
|
||||
})
|
||||
|
||||
it('should calculate the correct amount when a sendToken is defined', function () {
|
||||
assert.deepEqual(calcMaxAmount({
|
||||
sendToken: {
|
||||
decimals: 10,
|
||||
},
|
||||
tokenBalance: '64',
|
||||
}), 'e8d4a51000')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -139,7 +139,8 @@ class SendTransactionScreen extends PersistentForm {
|
|||
PersistentForm.call(this)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
this.getContractMethods()
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class SendTransactionScreen extends PersistentForm {
|
|||
<EnsInput
|
||||
name="address"
|
||||
placeholder="Recipient Address"
|
||||
onChange={() => this.recipientDidChange.bind(this)}
|
||||
onChange={this.recipientDidChange.bind(this)}
|
||||
network={network}
|
||||
identities={identities}
|
||||
addressBook={addressBook}
|
||||
|
|
|
@ -14,14 +14,14 @@ import ethUtil from 'ethereumjs-util'
|
|||
import SendProfile from './send-profile'
|
||||
import SendHeader from './send-header'
|
||||
import ErrorComponent from '../error'
|
||||
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
|
||||
import { getMetaMaskAccounts, getGasTotal, getTokenBalance, getCurrentEthBalance, getSendToken, getSendTo } from '../../../../ui/app/selectors'
|
||||
import * as Toast from '../toast'
|
||||
import AmountMaxButton from './amount-max-button'
|
||||
|
||||
const optionalDataLabelStyle = {
|
||||
background: '#ffffff',
|
||||
color: '#333333',
|
||||
marginTop: '16px',
|
||||
marginBottom: '16px',
|
||||
}
|
||||
const optionalDataValueStyle = {
|
||||
width: '100%',
|
||||
|
@ -33,11 +33,12 @@ class SendTransactionScreen extends PersistentForm {
|
|||
super(props)
|
||||
this.state = {
|
||||
pendingNonce: null,
|
||||
recipient: null,
|
||||
}
|
||||
}
|
||||
|
||||
async getPendingNonce () {
|
||||
const pendingNonce = await this.props.dispatch(actions.getPendingNonce(this.props.address))
|
||||
async fetchPendingNonce () {
|
||||
const pendingNonce = await this.props.getPendingNonce(this.props.address)
|
||||
this.setState({pendingNonce: pendingNonce})
|
||||
}
|
||||
|
||||
|
@ -50,6 +51,7 @@ class SendTransactionScreen extends PersistentForm {
|
|||
identities,
|
||||
addressBook,
|
||||
error,
|
||||
updateSendTo,
|
||||
} = props
|
||||
|
||||
return (
|
||||
|
@ -73,6 +75,8 @@ class SendTransactionScreen extends PersistentForm {
|
|||
network={network}
|
||||
identities={identities}
|
||||
addressBook={addressBook}
|
||||
value={this.state.recipient || ''}
|
||||
updateSendTo={updateSendTo}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
@ -88,14 +92,21 @@ class SendTransactionScreen extends PersistentForm {
|
|||
dataset={{
|
||||
persistentFormid: 'tx-amount',
|
||||
}}
|
||||
disabled={!!this.props.maxModeOn}
|
||||
value={this.props.amount || ''}
|
||||
onChange={(e) => {
|
||||
const newAmount = e.target.value
|
||||
this.props.updateSendAmount(newAmount)
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={this.onSubmit.bind(this)}>
|
||||
Next
|
||||
</button>
|
||||
</button>
|
||||
|
||||
</section>
|
||||
<section className="flex-row flex-left amount-max-container"><AmountMaxButton /></section>
|
||||
|
||||
<h3 className="flex-center"
|
||||
style={optionalDataLabelStyle}
|
||||
|
@ -111,6 +122,10 @@ class SendTransactionScreen extends PersistentForm {
|
|||
dataset={{
|
||||
persistentFormid: 'tx-data',
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const newTxData = e.target.value
|
||||
this.props.updateSendHexData(newTxData)
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
@ -137,16 +152,23 @@ class SendTransactionScreen extends PersistentForm {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.getPendingNonce()
|
||||
this._isMounted = true
|
||||
if (this._isMounted) {
|
||||
this.fetchPendingNonce()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(actions.displayWarning(''))
|
||||
this.props.displayWarning('')
|
||||
this.props.updateSendAmount(null)
|
||||
this.props.setMaxModeTo(false)
|
||||
this.props.updateSendTo('')
|
||||
this._isMounted = false
|
||||
}
|
||||
|
||||
navigateToAccounts (event) {
|
||||
event.stopPropagation()
|
||||
this.props.dispatch(actions.showAccountsPage())
|
||||
this.props.showAccountsPage()
|
||||
}
|
||||
|
||||
recipientDidChange (recipient, nickname) {
|
||||
|
@ -154,6 +176,7 @@ class SendTransactionScreen extends PersistentForm {
|
|||
recipient: recipient,
|
||||
nickname: nickname,
|
||||
})
|
||||
this.props.updateSendTo(recipient, nickname)
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
|
@ -175,14 +198,14 @@ class SendTransactionScreen extends PersistentForm {
|
|||
|
||||
if (isNaN(input) || input === '') {
|
||||
message = 'Invalid ether value.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
if (parts[1]) {
|
||||
const decimal = parts[1]
|
||||
if (decimal.length > 18) {
|
||||
message = 'Ether amount is too precise.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,32 +216,32 @@ class SendTransactionScreen extends PersistentForm {
|
|||
|
||||
if (value.gt(balance)) {
|
||||
message = 'Insufficient funds.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
if (input < 0) {
|
||||
message = 'Can not send negative amounts of ETH.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
if ((isInvalidChecksumAddress(recipient, this.props.network))) {
|
||||
message = 'Recipient address checksum is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
if ((!isValidAddress(recipient, this.props.network) && !txData) || (!recipient && !txData)) {
|
||||
message = 'Recipient address is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) {
|
||||
message = 'Transaction data must be hex string.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
return this.props.displayWarning(message)
|
||||
}
|
||||
|
||||
this.props.dispatch(actions.hideWarning())
|
||||
this.props.hideWarning()
|
||||
|
||||
this.props.dispatch(actions.addToAddressBook(recipient, nickname))
|
||||
this.props.addToAddressBook(recipient, nickname)
|
||||
|
||||
const txParams = {
|
||||
from: this.props.address,
|
||||
|
@ -229,19 +252,30 @@ class SendTransactionScreen extends PersistentForm {
|
|||
if (txData) txParams.data = txData
|
||||
if (txCustomNonce) txParams.nonce = '0x' + parseInt(txCustomNonce, 10).toString(16)
|
||||
|
||||
this.props.dispatch(actions.signTx(txParams))
|
||||
this.props.signTx(txParams)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const accounts = getMetaMaskAccounts(state)
|
||||
const balance = getCurrentEthBalance(state)
|
||||
const gasTotal = getGasTotal(state)
|
||||
const result = {
|
||||
send: state.metamask.send,
|
||||
address: state.metamask.selectedAddress,
|
||||
accounts,
|
||||
identities: state.metamask.identities,
|
||||
warning: state.appState.warning,
|
||||
network: state.metamask.network,
|
||||
addressBook: state.metamask.addressBook,
|
||||
balance,
|
||||
gasTotal,
|
||||
to: getSendTo(state),
|
||||
sendToken: getSendToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
amount: state.metamask.send.amount,
|
||||
maxModeOn: state.metamask.send.maxModeOn,
|
||||
blockGasLimit: state.metamask.currentBlockGasLimit,
|
||||
}
|
||||
|
||||
result.error = result.warning && result.warning.split('.')[0]
|
||||
|
@ -251,4 +285,19 @@ function mapStateToProps (state) {
|
|||
return result
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(SendTransactionScreen)
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
addToAddressBook: (recipient, nickname) => dispatch(actions.addToAddressBook(recipient, nickname)),
|
||||
showAccountsPage: () => dispatch(actions.showAccountsPage()),
|
||||
displayWarning: (msg) => dispatch(actions.displayWarning(msg)),
|
||||
hideWarning: () => dispatch(actions.hideWarning()),
|
||||
getPendingNonce: (address) => dispatch(actions.getPendingNonce(address)),
|
||||
signTx: (txParams) => dispatch(actions.signTx(txParams)),
|
||||
updateSendAmount: (amount) => dispatch(actions.updateSendAmount(amount)),
|
||||
setMaxModeTo: (maxMode) => dispatch(actions.setMaxModeTo(maxMode)),
|
||||
updateSendTo: (to, nickname) => dispatch(actions.updateSendTo(to, nickname)),
|
||||
updateSendHexData: (txData) => dispatch(actions.updateSendHexData(txData)),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTransactionScreen)
|
||||
|
|
|
@ -630,10 +630,6 @@ input.large-input {
|
|||
|
||||
/* accounts screen */
|
||||
|
||||
.identity-section {
|
||||
|
||||
}
|
||||
|
||||
.identity-section .identity-panel {
|
||||
background: #E9E9E9;
|
||||
border-bottom: 1px solid #B1B1B1;
|
||||
|
@ -670,10 +666,6 @@ input.large-input {
|
|||
flex-grow: 10;
|
||||
}
|
||||
|
||||
.name-label{
|
||||
|
||||
}
|
||||
|
||||
.unapproved-tx-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
@ -735,14 +727,14 @@ input.large-input {
|
|||
|
||||
/* Send Screen */
|
||||
|
||||
.send-screen {
|
||||
|
||||
}
|
||||
|
||||
.send-screen section {
|
||||
margin: 10px 30px;
|
||||
}
|
||||
|
||||
.send-screen section.amount-max-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.send-screen input {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
|
@ -750,17 +742,34 @@ input.large-input {
|
|||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.send__amount-max {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.send__amount-max input {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.secondary-description {
|
||||
font-family: 'Nunito Regular';
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
/* Ether Balance Widget */
|
||||
|
||||
.ether-balance-label {
|
||||
color: #ABA9AA;
|
||||
}
|
||||
|
||||
.icon-size{
|
||||
.icon-size {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.info{
|
||||
.info {
|
||||
font-family: 'Nunito Regular';
|
||||
padding-bottom: 10px;
|
||||
display: inline-block;
|
||||
|
@ -829,10 +838,6 @@ input.large-input {
|
|||
font-family: Nunito Semibold;
|
||||
}
|
||||
|
||||
.buy-radio {
|
||||
|
||||
}
|
||||
|
||||
.eth-warning{
|
||||
transition: opacity 400ms ease-in, transform 400ms ease-in;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const Transaction = require('ethereumjs-tx')
|
|||
|
||||
|
||||
const { hexToBn, bnToHex } = require('../../../../../app/scripts/lib/util')
|
||||
const TxUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils')
|
||||
import TxUtils from '../../../../../app/scripts/controllers/transactions/tx-gas-utils'
|
||||
|
||||
|
||||
describe('txUtils', function () {
|
||||
|
|
|
@ -1061,7 +1061,6 @@ function setGasTotal (gasTotal) {
|
|||
|
||||
function updateGasData ({
|
||||
blockGasLimit,
|
||||
recentBlocks,
|
||||
selectedAddress,
|
||||
selectedToken,
|
||||
to,
|
||||
|
@ -1100,11 +1099,13 @@ function updateGasData ({
|
|||
dispatch(actions.setGasTotal(gasEstimate))
|
||||
dispatch(updateSendErrors({ gasLoadingError: null }))
|
||||
dispatch(actions.gasLoadingFinished())
|
||||
return Promise.resolve()
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
|
||||
dispatch(actions.gasLoadingFinished())
|
||||
return Promise.reject()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,4 +248,5 @@ module.exports = {
|
|||
conversionMax,
|
||||
toNegative,
|
||||
subtractCurrencies,
|
||||
BIG_NUMBER_WEI_MULTIPLIER,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
const abi = require('human-standard-token-abi')
|
||||
import abi from 'human-standard-token-abi'
|
||||
import { pipe } from 'ramda'
|
||||
import {
|
||||
transactionsSelector,
|
||||
} from './selectors/transactions'
|
||||
const {
|
||||
import { addHexPrefix } from 'ethereumjs-util'
|
||||
import {
|
||||
conversionUtil,
|
||||
multiplyCurrencies,
|
||||
} = require('./conversion-util')
|
||||
} from './conversion-util'
|
||||
import {
|
||||
calcGasTotal,
|
||||
} from './components/send/send.utils'
|
||||
|
||||
const selectors = {
|
||||
getSelectedAddress,
|
||||
|
@ -33,6 +39,19 @@ const selectors = {
|
|||
preferencesSelector,
|
||||
getMetaMaskAccounts,
|
||||
getUsePhishDetect,
|
||||
getGasLimit,
|
||||
getGasPrice,
|
||||
getGasTotal,
|
||||
getGasPriceInHexWei,
|
||||
priceEstimateToWei,
|
||||
getCurrentEthBalance,
|
||||
getSendToken,
|
||||
getTokenBalance,
|
||||
getSendFromBalance,
|
||||
getSendFromObject,
|
||||
getSendTo,
|
||||
getSendHexData,
|
||||
getTargetAccount,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
@ -151,12 +170,20 @@ function getSendFrom (state) {
|
|||
return state.metamask.send.from
|
||||
}
|
||||
|
||||
function getSendTo (state) {
|
||||
return state.metamask.send.to
|
||||
}
|
||||
|
||||
function getSendAmount (state) {
|
||||
return state.metamask.send.amount
|
||||
}
|
||||
|
||||
function getSendMaxModeState (state) {
|
||||
return state.metamask.send.maxModeOn
|
||||
return state.metamask.send.maxModeOn || false
|
||||
}
|
||||
|
||||
function getSendHexData (state) {
|
||||
return state.metamask.send.data
|
||||
}
|
||||
|
||||
function getCurrentCurrency (state) {
|
||||
|
@ -203,3 +230,62 @@ function getTotalUnapprovedCount ({ metamask }) {
|
|||
function preferencesSelector ({ metamask }) {
|
||||
return metamask.preferences
|
||||
}
|
||||
|
||||
function getGasLimit (state) {
|
||||
return state.metamask.send.gasLimit || '0'
|
||||
}
|
||||
|
||||
function getGasPrice (state) {
|
||||
return state.metamask.send.gasPrice
|
||||
}
|
||||
|
||||
function getGasTotal (state) {
|
||||
return calcGasTotal(getGasLimit(state), getGasPrice(state))
|
||||
}
|
||||
|
||||
function priceEstimateToWei (priceEstimate) {
|
||||
return conversionUtil(priceEstimate, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'hex',
|
||||
fromDenomination: 'GWEI',
|
||||
toDenomination: 'WEI',
|
||||
numberOfDecimals: 9,
|
||||
})
|
||||
}
|
||||
|
||||
function getGasPriceInHexWei (price) {
|
||||
return pipe(
|
||||
(x) => conversionUtil(x, { fromNumericBase: 'dec', toNumericBase: 'hex' }),
|
||||
priceEstimateToWei,
|
||||
addHexPrefix,
|
||||
)(price)
|
||||
}
|
||||
|
||||
function getCurrentEthBalance (state) {
|
||||
return getCurrentAccountWithSendEtherInfo(state).balance
|
||||
}
|
||||
|
||||
function getSendToken (state) {
|
||||
return state.metamask.send.token
|
||||
}
|
||||
|
||||
function getTokenBalance (state) {
|
||||
return state.metamask.send.tokenBalance
|
||||
}
|
||||
|
||||
function getSendFromBalance (state) {
|
||||
const fromAccount = getSendFromObject(state)
|
||||
return fromAccount.balance
|
||||
}
|
||||
|
||||
function getSendFromObject (state) {
|
||||
const fromAddress = getSendFrom(state)
|
||||
return fromAddress
|
||||
? getTargetAccount(state, fromAddress)
|
||||
: getSelectedAccount(state)
|
||||
}
|
||||
|
||||
function getTargetAccount (state, targetAddress) {
|
||||
const accounts = getMetaMaskAccounts(state)
|
||||
return accounts[targetAddress]
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ class WelcomeScreen extends Component {
|
|||
this.animationEventEmitter = new EventEmitter()
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount () {
|
||||
const { history, welcomeScreenSeen } = this.props
|
||||
|
||||
if (welcomeScreenSeen) {
|
||||
|
|
Loading…
Reference in New Issue