Merge pull request #395 from poanetwork/develop

NW release 5.1.1
This commit is contained in:
Victor Baranov 2020-06-15 19:13:12 +03:00 committed by GitHub
commit 7f46b1cd45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2584 additions and 1565 deletions

View File

@ -2,6 +2,19 @@
## Current Master
## 5.1.1 Mon Jun 15 2020
- [#393](https://github.com/poanetwork/nifty-wallet/pull/393) - (Feature) "Send all" option for tokens transfer
- [#391](https://github.com/poanetwork/nifty-wallet/pull/391) - (Feature) Gas price oracles npm package integration
- [#389](https://github.com/poanetwork/nifty-wallet/pull/389) - (Feature) Support 24 words mnemonic phrase
- [#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
- [#382](https://github.com/poanetwork/nifty-wallet/pull/382) - (Fix) replace vulnerable npm dependencies with newer versions of packages, update chromedriver to match the latest Google Chrome release
- [#381](https://github.com/poanetwork/nifty-wallet/pull/381) - (Feature) Add RNS integration
- [#381](https://github.com/poanetwork/nifty-wallet/pull/381) - (Fix) ENS/RNS integration when sending tokens
## 5.1.0 Tue May 12 2020
- [#356](https://github.com/poanetwork/nifty-wallet/pull/356) - (Backwards-compatibility feature) Custom derivation paths and access to funds in accounts derived from ETH dPath

View File

@ -355,6 +355,9 @@
"ensNameNotFound": {
"message": "ENS name not found"
},
"rnsNameNotFound": {
"message": "RNS name not found"
},
"enterPassword": {
"message": "Enter password"
},

View File

@ -349,6 +349,9 @@
"ensNameNotFound": {
"message": "Nom ENS inconnu"
},
"rnsNameNotFound": {
"message": "Nom RNS inconnu"
},
"enterPassword": {
"message": "Entrez votre mot de passe"
},

View File

@ -313,6 +313,9 @@
"ensNameNotFound": {
"message": "Nou pa jwenn non ENS ou a"
},
"rnsNameNotFound": {
"message": "Nou pa jwenn non RNS ou a"
},
"enterPassword": {
"message": "Mete modpas"
},

View File

@ -352,6 +352,9 @@
"ensNameNotFound": {
"message": "Nome ENS non trovato"
},
"rnsNameNotFound": {
"message": "Nome RNS non trovato"
},
"enterPassword": {
"message": "Inserisci password"
},

View File

@ -349,6 +349,9 @@
"ensNameNotFound": {
"message": "ENS 이름을 찾을 수 없습니다"
},
"rnsNameNotFound": {
"message": "RNS 이름을 찾을 수 없습니다"
},
"enterPassword": {
"message": "비밀번호를 입력해주세요"
},

View File

@ -313,6 +313,9 @@
"ensNameNotFound": {
"message": "Nie znaleziono nazwy ENS"
},
"rnsNameNotFound": {
"message": "Nie znaleziono nazwy RNS"
},
"enterPassword": {
"message": "Wpisz hasło"
},

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "5.1.0",
"version": "5.1.1",
"manifest_version": 2,
"author": "POA Network",
"description": "__MSG_appDescription__",

View File

@ -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'

View File

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

View File

@ -91,7 +91,7 @@ function getExchanges ({network, amount, address}) {
return [
{
name: 'Binance',
link: 'https://www.binance.com/en/trade/POA_ETH',
link: 'https://www.binance.com/en/trade/POA_BTC',
},
{
name: 'BiBox',

View File

@ -61,6 +61,7 @@ import nanoid from 'nanoid'
const { importTypes } = require('../../old-ui/app/accounts/import/enums')
const { LEDGER, TREZOR } = require('../../old-ui/app/components/connect-hardware/enum')
const { ifPOA, ifRSK, getNetworkID, getDPath, setDPath } = require('../../old-ui/app/util')
const { GasPriceOracle } = require('gas-price-oracle')
import {
PhishingController,
@ -443,7 +444,8 @@ 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
createShapeShiftTx: this.createShapeShiftTx.bind(this),
@ -1869,10 +1871,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
@ -1967,23 +1968,30 @@ module.exports = class MetamaskController extends EventEmitter {
*/
getGasPriceFromOracles (networkId) {
return new Promise(async (resolve, reject) => {
const gasPriceOracleETC = 'https://gasprice-etc.poa.network'
const gasPriceOracleETH = 'https://gasprice.poa.network'
const gasPriceOracle = networkId === CLASSIC_CODE ?
gasPriceOracleETC : networkId === MAINNET_CODE ? gasPriceOracleETH : null
try {
if (gasPriceOracle) {
const response = await fetch(gasPriceOracle)
if (networkId === MAINNET_CODE) {
const oracle = new GasPriceOracle()
// optional fallbackGasPrices
const fallbackGasPrices = {
instant: 70, fast: 31, standard: 20, low: 7,
}
oracle.gasPrices(fallbackGasPrices).then((gasPrices) => {
gasPrices && (gasPrices.standard || gasPrices.fast) ? resolve(gasPrices.standard || gasPrices.fast) : reject()
})
} else if (networkId === CLASSIC_CODE) {
const gasPriceOracleETC = 'https://gasprice-etc.poa.network'
try {
const response = await fetch(gasPriceOracleETC)
const parsedResponse = await response.json()
if (parsedResponse && (parsedResponse.standard || parsedResponse.fast)) {
resolve(parsedResponse.standard || parsedResponse.fast)
} else {
reject()
reject('Empty response from gas price oracle')
}
} catch (error) {
reject(error)
}
} catch (error) {
reject(error)
} else {
reject(`No gas price oracles for ${networkId}`)
}
})
}

View File

@ -34,7 +34,8 @@ class ConfirmSeedScreen extends Component {
}
}
componentWillMount () {
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {

View File

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

View File

@ -41,7 +41,8 @@ class ImportSeedPhraseScreen extends Component {
let seedPhraseError = null
if (seedPhrase) {
if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
const wordsCount = this.parseSeedPhrase(seedPhrase).split(' ').length
if (wordsCount !== 12 && wordsCount !== 24) {
seedPhraseError = this.context.t('seedPhraseReq')
} else if (!validateMnemonic(seedPhrase)) {
seedPhraseError = this.context.t('invalidSeedPhrase')

View File

@ -58,7 +58,8 @@ class BackupPhraseScreen extends Component {
}
}
componentWillMount () {
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {

View File

@ -26,7 +26,8 @@ export class ShapeShiftForm extends Component {
isLoading: false,
};
componentWillMount () {
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
this.props.shapeShiftSubview()
}

View File

@ -159,7 +159,7 @@ AddSuggestedTokenScreen.prototype.render = function () {
)
}
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
AddSuggestedTokenScreen.prototype.UNSAFE_componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
}

View File

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

View File

@ -1,200 +1,240 @@
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 RNSRegistryData = require('@rsksmart/rns-registry/RNSRegistryData.json')
const ensRE = /.+\..+$/
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const log = require('loglevel')
const { isValidENSAddress } = require('../util')
const { isValidENSAddress, isValidRNSAddress } = require('../util')
const {
RSK_CODE,
RSK_TESTNET_CODE,
} = require('../../../app/scripts/controllers/network/enums')
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,
}
module.exports = EnsInput
render () {
const props = this.props
inherits(EnsInput, Component)
function EnsInput () {
Component.call(this)
}
function onInputChange () {
const recipient = document.querySelector('input[name="address"]').value
this.props.updateSendTo(recipient, ' ')
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
const networkHasRnsSupport = getNetworkRnsSupport(network)
if (!networkHasEnsSupport && !networkHasRnsSupport) return
EnsInput.prototype.render = function () {
const props = this.props
if (recipient.match(ensRE) === null) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
toError: null,
})
}
function onInputChange () {
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
const networkHasRnsSupport = getNetworkRnsSupport(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, 'ENS'), 200)
} else if (networkHasRnsSupport) {
const registryAddress = getRnsRegistryAddress(network)
const provider = global.ethereumProvider
this.ens = new ENS({ provider, network, registryAddress })
this.checkName = debounce(this.lookupEnsName.bind(this, 'RNS'), 200)
}
}
lookupEnsName (nameService) {
const recipient = document.querySelector('input[name="address"]').value
if (recipient.match(ensRE) === null) {
return this.setState({
const { ensResolution } = this.state
log.info(`${nameService} 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) || isValidRNSAddress(recipient)) &&
reason.message === 'ENS name not defined.'
) {
setStateObj.hoverText = '${nameService} name not found'
setStateObj.toError = `${nameService.toLowerCase()}NameNotFound`
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])
}
function getNetworkRnsSupport (network) {
return (network === RSK_CODE || network === RSK_TESTNET_CODE)
}
function getRnsRegistryAddress (network) {
if (network === RSK_CODE) {
return RNSRegistryData.address.rskMainnet
}
if (network === RSK_TESTNET_CODE) {
return RNSRegistryData.address.rskTestnet
};
return
}
module.exports = EnsInput

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import { 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) {
const tokenBalanceBN = new BigNumber(tokenBalance.toString())
maxBalance = tokenBalanceBN.div(multiplier).toString()
} 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
}

View File

@ -0,0 +1 @@
export { default } from './amount-max-button.container'

View File

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

View File

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

View File

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

View File

@ -139,7 +139,8 @@ class SendTransactionScreen extends PersistentForm {
PersistentForm.call(this)
}
componentWillMount () {
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
this.getContractMethods()
}

View File

@ -18,7 +18,8 @@ import log from 'loglevel'
import SendProfile from './send-profile'
import SendHeader from './send-header'
import ErrorComponent from '../error'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
import { getMetaMaskAccounts, getSendToken, getSendTo, getTokenBalance, getSendTokenContract } from '../../../../ui/app/selectors'
import AmountMaxButton from './amount-max-button'
class SendTransactionScreen extends PersistentForm {
constructor (props) {
@ -30,13 +31,12 @@ class SendTransactionScreen extends PersistentForm {
balance: 0,
decimals: 0,
},
amount: '',
isLoading: true,
}
PersistentForm.call(this)
}
render () {
const { isLoading, token, amount } = this.state
const { isLoading, token } = this.state
if (isLoading) {
return (
<Loading isLoading={isLoading} loadingMessage="Loading..." />
@ -50,6 +50,7 @@ class SendTransactionScreen extends PersistentForm {
identities,
addressBook,
error,
updateSendTo,
} = props
const nextDisabled = token.balance <= 0
@ -63,22 +64,24 @@ 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}
updateSendTo={updateSendTo}
/>
</section>
<section className="flex-row flex-center">
<input className="large-input"
name="amount"
value={amount}
value={this.props.amount || ''}
onChange={(e) => this.amountDidChange(e.target.value)}
placeholder="Amount"
type="number"
style={{
marginRight: '6px',
}}
disabled={!!this.props.maxModeOn}
/>
<button
onClick={() => this.onSubmit()}
@ -86,13 +89,22 @@ class SendTransactionScreen extends PersistentForm {
>Next
</button>
</section>
<section className="flex-row flex-left amount-max-container"><AmountMaxButton /></section>
</div>
)
}
componentDidMount () {
this.getTokensMetadata()
.then(() => {
.then((token) => {
this.props.updateSendToken(token)
const {
sendToken,
tokenContract,
address,
} = this.props
this.props.updateSendTokenBalance({sendToken, tokenContract, address})
this.createFreshTokenTracker()
})
}
@ -102,16 +114,17 @@ class SendTransactionScreen extends PersistentForm {
this.tokenInfoGetter = tokenInfoGetter()
const { tokenAddress, network } = this.props
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(tokenAddress)
const token = {
address: tokenAddress,
network,
symbol,
decimals,
}
this.setState({
token: {
address: tokenAddress,
network,
symbol,
decimals,
},
token,
})
return Promise.resolve()
return Promise.resolve(token)
}
componentWillUnmount () {
@ -120,6 +133,9 @@ class SendTransactionScreen extends PersistentForm {
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
this.props.updateSendAmount(null)
this.props.setMaxModeTo(false)
this.props.updateSendTo('')
}
createFreshTokenTracker () {
@ -176,14 +192,13 @@ class SendTransactionScreen extends PersistentForm {
}
amountDidChange (amount) {
this.setState({
amount,
})
this.props.updateSendAmount(amount)
}
async onSubmit () {
const state = this.state || {}
const { token, amount } = state
const { token } = state
const { amount } = this.props
let recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
let nickname = state.nickname || ' '
if (typeof recipient === 'object') {
@ -284,6 +299,12 @@ const mapStateToProps = (state) => {
network: state.metamask.network,
addressBook: state.metamask.addressBook,
tokenAddress: state.appState.currentView.tokenAddress,
to: getSendTo(state),
sendToken: getSendToken(state),
amount: state.metamask.send.amount,
maxModeOn: state.metamask.send.maxModeOn,
tokenBalance: getTokenBalance(state),
tokenContract: getSendTokenContract(state),
}
result.error = result.warning && result.warning.split('.')[0]
@ -307,6 +328,11 @@ const mapDispatchToProps = dispatch => {
txParams,
confTxScreenParams,
) => dispatch(actions.signTokenTx(tokenAddress, toAddress, tokensValueWithDec, txParams, confTxScreenParams)),
updateSendTokenBalance: props => dispatch(actions.updateSendTokenBalance(props)),
setMaxModeTo: maxMode => dispatch(actions.setMaxModeTo(maxMode)),
updateSendAmount: amount => dispatch(actions.updateSendAmount(amount)),
updateSendTo: (to, nickname) => dispatch(actions.updateSendTo(to, nickname)),
updateSendToken: token => dispatch(actions.updateSendToken(token)),
}
}

View File

@ -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%',
@ -29,6 +29,19 @@ const optionalDataValueStyle = {
}
class SendTransactionScreen extends PersistentForm {
constructor (props) {
super(props)
this.state = {
pendingNonce: null,
recipient: null,
}
}
async fetchPendingNonce () {
const pendingNonce = await this.props.getPendingNonce(this.props.address)
this.setState({pendingNonce: pendingNonce})
}
render () {
this.persistentFormParentId = 'send-tx-form'
@ -38,6 +51,7 @@ class SendTransactionScreen extends PersistentForm {
identities,
addressBook,
error,
updateSendTo,
} = props
return (
@ -61,6 +75,8 @@ class SendTransactionScreen extends PersistentForm {
network={network}
identities={identities}
addressBook={addressBook}
value={this.state.recipient || ''}
updateSendTo={updateSendTo}
/>
</section>
@ -76,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}
@ -99,6 +122,10 @@ class SendTransactionScreen extends PersistentForm {
dataset={{
persistentFormid: 'tx-data',
}}
onChange={(e) => {
const newTxData = e.target.value
this.props.updateSendHexData(newTxData)
}}
/>
</section>
@ -117,19 +144,31 @@ class SendTransactionScreen extends PersistentForm {
dataset={{
persistentFormid: 'tx-custom-nonce',
}}
defaultValue={this.state.pendingNonce}
/>
</section>
</div>
)
}
componentDidMount () {
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) {
@ -137,6 +176,7 @@ class SendTransactionScreen extends PersistentForm {
recipient: recipient,
nickname: nickname,
})
this.props.updateSendTo(recipient, nickname)
}
onSubmit () {
@ -158,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)
}
}
@ -176,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,
@ -212,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]
@ -234,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)

View File

@ -40,6 +40,7 @@
flex: 1;
justify-content: center;
text-transform: uppercase;
line-height: 54px;
}
.hw-connect__connect-btn.disabled {
cursor: not-allowed;

View File

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

View File

@ -1,15 +1,83 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
import { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
const h = require('react-hyperscript')
const actions = require('../../../../ui/app/actions')
const exportAsFile = require('../../util').exportAsFile
const { confirmSeedWords, showAccountDetail } = require('../../../../ui/app/actions')
const { exportAsFile } = require('../../util')
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
class CreateVaultCompleteScreen extends Component {
inherits(CreateVaultCompleteScreen, Component)
function CreateVaultCompleteScreen () {
Component.call(this)
static propTypes = {
seed: PropTypes.string,
cachedSeed: PropTypes.string,
confirmSeedWords: PropTypes.func,
showAccountDetail: PropTypes.func,
};
render () {
const state = this.props
const seed = state.seed || state.cachedSeed || ''
const wordsCount = seed.split(' ').length
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h3.flex-center.section-title', {
style: {
background: '#ffffff',
color: '#333333',
marginBottom: 8,
width: '100%',
padding: '30px 6px 6px 6px',
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
margin: '10px 30px',
textAlign: 'center',
},
}, [
h('div.error', `These ${wordsCount} words are the only way to restore your Nifty Wallet accounts.\nSave them somewhere safe and secret.`),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seed,
}),
h('button', {
onClick: () => this.confirmSeedWords()
.then(account => this.showAccountDetail(account)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button', {
onClick: () => exportAsFile(`Nifty Wallet Seed Words`, seed),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
])
)
}
confirmSeedWords () {
return this.props.confirmSeedWords()
}
showAccountDetail (account) {
return this.props.showAccountDetail(account)
}
}
function mapStateToProps (state) {
@ -19,71 +87,11 @@ function mapStateToProps (state) {
}
}
CreateVaultCompleteScreen.prototype.render = function () {
const state = this.props
const seed = state.seed || state.cachedSeed || ''
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// // subtitle and nav
// h('.section-title.flex-row.flex-center', [
// h('h2.page-subtitle', 'Vault Created'),
// ]),
h('h3.flex-center.section-title', {
style: {
background: '#ffffff',
color: '#333333',
marginBottom: 8,
width: '100%',
padding: '30px 6px 6px 6px',
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
margin: '10px 30px',
textAlign: 'center',
},
}, [
h('div.error', 'These 12 words are the only way to restore your Nifty Wallet accounts.\nSave them somewhere safe and secret.'),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seed,
}),
h('button', {
onClick: () => this.confirmSeedWords()
.then(account => this.showAccountDetail(account)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button', {
onClick: () => exportAsFile(`Nifty Wallet Seed Words`, seed),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
])
)
function mapDispatchToProps (dispatch) {
return {
confirmSeedWords: () => dispatch(confirmSeedWords()),
showAccountDetail: (account) => dispatch(showAccountDetail(account)),
}
}
CreateVaultCompleteScreen.prototype.confirmSeedWords = function () {
return this.props.dispatch(actions.confirmSeedWords())
}
CreateVaultCompleteScreen.prototype.showAccountDetail = function (account) {
return this.props.dispatch(actions.showAccountDetail(account))
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateVaultCompleteScreen)

View File

@ -179,8 +179,9 @@ RestoreVaultScreen.prototype.createNewVaultAndRestore = function () {
this.props.dispatch(actions.displayWarning(this.warning))
return
}
if (seed.split(' ').length !== 12) {
this.warning = 'seed phrases are 12 words long'
const wordsCount = seed.split(' ').length
if (wordsCount !== 12 && wordsCount !== 24) {
this.warning = 'seed phrases are 12 or 24 words long'
this.props.dispatch(actions.displayWarning(this.warning))
return
}

View File

@ -67,6 +67,7 @@ module.exports = {
isAllOneCase,
isValidAddress,
isValidENSAddress,
isValidRNSAddress,
numericBalance,
parseBalance,
formatBalance,
@ -139,6 +140,10 @@ function isValidENSAddress (address) {
return address.match(/^.{7,}\.(eth|test)$/)
}
function isValidRNSAddress (address) {
return address.match(/^[a-z0-9]+\.rsk$/)
}
function isInvalidChecksumAddress (address, network) {
const prefixed = ethUtil.addHexPrefix(address)
if (address === '0x0000000000000000000000000000000000000000') return false

2637
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"@material-ui/core": "^4.1.1",
"@rsksmart/rns-registry": "^1.0.4",
"@rsksmart/rsk-contract-metadata": "github:rsksmart/rsk-contract-metadata#master",
"@rsksmart/rsk-testnet-contract-metadata": "github:rsksmart/rsk-testnet-contract-metadata#master",
"@zxing/library": "^0.8.0",
@ -142,6 +143,7 @@
"fast-levenshtein": "^2.0.6",
"fuse.js": "^3.2.0",
"gaba": "^1.9.3",
"gas-price-oracle": "^0.1.4",
"human-standard-token-abi": "^2.0.0",
"idb-global": "^2.1.0",
"iframe-stream": "^3.0.0",
@ -155,7 +157,7 @@
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
"metamascara": "^2.0.0",
"mkdirp": "^0.5.1",
"mkdirp": "^0.5.5",
"multihashes": "^0.4.12",
"nanoid": "^2.1.6",
"nifty-wallet-inpage-provider": "github:poanetwork/nifty-wallet-inpage-provider#1.5.1",
@ -228,7 +230,7 @@
"@storybook/addon-info": "^5.3.14",
"@storybook/addon-knobs": "^5.3.14",
"@storybook/react": "^5.3.14",
"addons-linter": "^1.22.0",
"addons-linter": "^1.25.0",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.0.6",
"babelify": "^10.0.0",
@ -236,7 +238,7 @@
"browserify": "^16.2.3",
"browserify-derequire": "^1.0.1",
"chai": "^4.1.0",
"chromedriver": "^80.0.2",
"chromedriver": "^83.0.0",
"clipboardy": "^1.2.3",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
@ -275,14 +277,14 @@
"gulp-util": "^3.0.7",
"gulp-watch": "^5.0.1",
"gulp-zip": "^4.0.0",
"http-server": "^0.12.1",
"http-server": "^0.12.3",
"image-size": "^0.6.2",
"isomorphic-fetch": "^2.2.1",
"jsdoc": "^3.6.3",
"jsdom": "^11.2.0",
"jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1",
"karma": "^4.4.1",
"karma": "^5.0.9",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
"karma-firefox-launcher": "^1.0.1",
@ -293,7 +295,7 @@
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^2.0.0",
"nock": "^9.0.14",
"node-sass": "^4.12.0",
"node-sass": "^4.14.1",
"nyc": "^15.0.0",
"path": "^0.12.7",
"png-file-stream": "^1.1.0",
@ -317,7 +319,7 @@
"source-map": "^0.7.2",
"static-server": "^2.2.1",
"style-loader": "^0.21.0",
"stylelint": "^9.10.1",
"stylelint": "^13.6.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.16.0",

View File

@ -1,6 +1,7 @@
const assert = require('assert')
const { screens, menus, NETWORKS } = require('../elements')
const testSeedPhrase = 'horn among position unable audit puzzle cannon apology gun autumn plug parrot'
const test24SeedPhrase = 'gravity trophy shrimp suspect sheriff avocado label trust dove tragic pitch title network myself spell task protect smooth sword diary brain blossom under bulb'
const importGanacheSeedPhrase = async (f, account2, password) => {
it('logs out', async () => {
@ -12,6 +13,38 @@ const importGanacheSeedPhrase = async (f, account2, password) => {
await logOut.click()
})
it('restores from 24 seed phrase', async () => {
const restoreSeedLink = await f.waitUntilShowUp(screens.lock.linkRestore)
assert.equal(await restoreSeedLink.getText(), screens.lock.linkRestoreText)
await restoreSeedLink.click()
})
it('adds 24 words seed phrase', async () => {
const seedTextArea = await f.waitUntilShowUp(screens.restoreVault.textArea)
await seedTextArea.sendKeys(test24SeedPhrase)
let field = await f.driver.findElement(screens.restoreVault.fieldPassword)
await field.sendKeys(password)
field = await f.driver.findElement(screens.restoreVault.fieldPasswordConfirm)
await field.sendKeys(password)
field = await f.waitUntilShowUp(screens.restoreVault.buttos.ok)
await f.click(field)
})
it('balance renders', async () => {
const balance = await f.waitUntilShowUp(screens.main.balance)
assert.equal(await balance.getText(), '0', "balance isn't correct")
})
it('logs out', async () => {
await f.setProvider(NETWORKS.LOCALHOST)
const menu = await f.waitUntilShowUp(menus.sandwich.menu)
await menu.click()
const logOut = await f.waitUntilShowUp(menus.sandwich.logOut)
assert.equal(await logOut.getText(), menus.sandwich.textLogOut)
await logOut.click()
})
it('restores from seed phrase', async () => {
const restoreSeedLink = await f.waitUntilShowUp(screens.lock.linkRestore)
assert.equal(await restoreSeedLink.getText(), screens.lock.linkRestoreText)

View File

@ -71,7 +71,7 @@ describe('', function () {
assert.deepEqual(exchanges, [
{
name: 'Binance',
link: 'https://www.binance.com/en/trade/POA_ETH',
link: 'https://www.binance.com/en/trade/POA_BTC',
},
{
name: 'BiBox',

View File

@ -137,7 +137,7 @@ describe('MetaMaskController', function () {
},
}
const gasPrice = await metamaskController.getGasPrice()
const gasPrice = await metamaskController.getGasPriceFromBlocks(1)
assert.equal(gasPrice, '0x174876e800', 'accurately estimates 65th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController

View File

@ -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 () {

View File

@ -188,6 +188,7 @@ const actions = {
signTokenTx: signTokenTx,
updateTransaction,
updateAndApproveTx,
getPendingNonce,
cancelTx,
cancelTxs,
completedTx: completedTx,
@ -211,6 +212,7 @@ const actions = {
UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
UPDATE_SEND: 'UPDATE_SEND',
UPDATE_SEND_TOKEN: 'UPDATE_SEND_TOKEN',
CLEAR_SEND: 'CLEAR_SEND',
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
@ -229,6 +231,7 @@ const actions = {
updateSendMemo,
setMaxModeTo,
updateSend,
updateSendToken,
updateSendErrors,
clearSend,
setSelectedAddress,
@ -1060,7 +1063,6 @@ function setGasTotal (gasTotal) {
function updateGasData ({
blockGasLimit,
recentBlocks,
selectedAddress,
selectedToken,
to,
@ -1099,11 +1101,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()
})
}
}
@ -1131,7 +1135,7 @@ function showChooseContractExecutorPage ({methodSelected, methodABI, inputValues
}
function updateSendTokenBalance ({
selectedToken,
sendToken,
tokenContract,
address,
}) {
@ -1142,7 +1146,7 @@ function updateSendTokenBalance ({
return tokenBalancePromise
.then(usersToken => {
if (usersToken) {
const newTokenBalance = calcTokenBalance({ selectedToken, usersToken })
const newTokenBalance = calcTokenBalance({ sendToken, usersToken })
dispatch(setSendTokenBalance(newTokenBalance.toString(10)))
}
})
@ -1216,6 +1220,13 @@ function updateSend (newSend) {
}
}
function updateSendToken (token) {
return {
type: actions.UPDATE_SEND_TOKEN,
value: token,
}
}
function clearSend () {
return {
type: actions.CLEAR_SEND,
@ -1304,6 +1315,24 @@ function updateAndApproveTx (txData) {
}
}
function getPendingNonce (address) {
log.info('actions: getPendingNonce')
return (dispatch) => {
log.debug(`actions calling background.getPendingNonce`)
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.getPendingNonce(address, (err, nonce) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideLoadingIndication())
resolve(nonce)
})
})
}
}
function completedTx (id) {
return {
type: actions.COMPLETED_TX,

View File

@ -248,4 +248,5 @@ module.exports = {
conversionMax,
toNegative,
subtractCurrencies,
BIG_NUMBER_WEI_MULTIPLIER,
}

View File

@ -283,6 +283,35 @@ function reduceMetamask (state, action) {
},
})
case actions.UPDATE_SEND_TOKEN:
const newSend = {
...metamaskState.send,
token: action.value,
}
// erase token-related state when switching back to native currency
if (newSend.editingTransactionId && !newSend.token) {
const unapprovedTx = newSend?.unapprovedTxs?.[newSend.editingTransactionId] || {}
const txParams = unapprovedTx.txParams || {}
Object.assign(newSend, {
tokenBalance: null,
balance: '0',
from: unapprovedTx.from || '',
unapprovedTxs: {
...newSend.unapprovedTxs,
[newSend.editingTransactionId]: {
...unapprovedTx,
txParams: {
...txParams,
data: '',
},
},
},
})
}
return Object.assign(metamaskState, {
send: newSend,
})
case actions.CLEAR_SEND:
return extend(metamaskState, {
send: {

View File

@ -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,21 @@ const selectors = {
preferencesSelector,
getMetaMaskAccounts,
getUsePhishDetect,
getGasLimit,
getGasPrice,
getGasTotal,
getGasPriceInHexWei,
priceEstimateToWei,
getCurrentEthBalance,
getSendToken,
getSendTokenAddress,
getSendTokenContract,
getTokenBalance,
getSendFromBalance,
getSendFromObject,
getSendTo,
getSendHexData,
getTargetAccount,
}
module.exports = selectors
@ -151,12 +172,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 +232,73 @@ 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 getSendTokenAddress (state) {
return getSendToken(state)?.address
}
function getSendTokenContract (state) {
const sendTokenAddress = getSendTokenAddress(state)
return sendTokenAddress
? global.eth.contract(abi).at(sendTokenAddress)
: null
}
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]
}

View File

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