commit
7f46b1cd45
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -355,6 +355,9 @@
|
|||
"ensNameNotFound": {
|
||||
"message": "ENS name not found"
|
||||
},
|
||||
"rnsNameNotFound": {
|
||||
"message": "RNS name not found"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Enter password"
|
||||
},
|
||||
|
|
|
@ -349,6 +349,9 @@
|
|||
"ensNameNotFound": {
|
||||
"message": "Nom ENS inconnu"
|
||||
},
|
||||
"rnsNameNotFound": {
|
||||
"message": "Nom RNS inconnu"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Entrez votre mot de passe"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -352,6 +352,9 @@
|
|||
"ensNameNotFound": {
|
||||
"message": "Nome ENS non trovato"
|
||||
},
|
||||
"rnsNameNotFound": {
|
||||
"message": "Nome RNS non trovato"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Inserisci password"
|
||||
},
|
||||
|
|
|
@ -349,6 +349,9 @@
|
|||
"ensNameNotFound": {
|
||||
"message": "ENS 이름을 찾을 수 없습니다"
|
||||
},
|
||||
"rnsNameNotFound": {
|
||||
"message": "RNS 이름을 찾을 수 없습니다"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "비밀번호를 입력해주세요"
|
||||
},
|
||||
|
|
|
@ -313,6 +313,9 @@
|
|||
"ensNameNotFound": {
|
||||
"message": "Nie znaleziono nazwy ENS"
|
||||
},
|
||||
"rnsNameNotFound": {
|
||||
"message": "Nie znaleziono nazwy RNS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Wpisz hasło"
|
||||
},
|
||||
|
|
|
@ -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__",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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,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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
flex: 1;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
line-height: 54px;
|
||||
}
|
||||
.hw-connect__connect-btn.disabled {
|
||||
cursor: not-allowed;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -248,4 +248,5 @@ module.exports = {
|
|||
conversionMax,
|
||||
toNegative,
|
||||
subtractCurrencies,
|
||||
BIG_NUMBER_WEI_MULTIPLIER,
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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