* origin/develop: (35 commits) Delete unused InfuraController & tests (#8773) Permissions: Do not display HTTP/HTTPS URL schemes for unique hosts (#8768) Refactor confirm approve page (#8757) blocklisted -> blocked Update app/scripts/contentscript.js blacklist -> blocklist; whitelist -> safelist replace blacklist with blocklist Delete unused transaction history test state (#8769) fix-formatting-of-gif (#8767) Order accounts on connect page (#8762) add gif for loading dev build (#8766) Bump websocket-extensions from 0.1.3 to 0.1.4 (#8759) Fix prop type mismatch (#8754) use grid template to position list item (#8753) Fix account menu entry for imported accounts (#8747) Fix permissions connect close and redirect behavior (#8751) Refactor `TokenBalance` component (#8752) Fix 'Remove account' in Account Options menu (#8748) move activation logic into token rates controller (#8744) asset outdated warning inline on full screen (#8734) ...feature/default_network_editable
commit
3604c3519c
@ -1,39 +0,0 @@ |
||||
import ObservableStore from 'obs-store' |
||||
import log from 'loglevel' |
||||
|
||||
// every ten minutes
|
||||
const POLLING_INTERVAL = 10 * 60 * 1000 |
||||
|
||||
export default class InfuraController { |
||||
|
||||
constructor (opts = {}) { |
||||
const initState = Object.assign({ |
||||
infuraNetworkStatus: {}, |
||||
}, opts.initState) |
||||
this.store = new ObservableStore(initState) |
||||
} |
||||
|
||||
//
|
||||
// PUBLIC METHODS
|
||||
//
|
||||
|
||||
// Responsible for retrieving the status of Infura's nodes. Can return either
|
||||
// ok, degraded, or down.
|
||||
async checkInfuraNetworkStatus () { |
||||
const response = await window.fetch('https://api.infura.io/v1/status/metamask') |
||||
const parsedResponse = await response.json() |
||||
this.store.updateState({ |
||||
infuraNetworkStatus: parsedResponse, |
||||
}) |
||||
return parsedResponse |
||||
} |
||||
|
||||
scheduleInfuraNetworkCheck () { |
||||
if (this.conversionInterval) { |
||||
clearInterval(this.conversionInterval) |
||||
} |
||||
this.conversionInterval = setInterval(() => { |
||||
this.checkInfuraNetworkStatus().catch(log.warn) |
||||
}, POLLING_INTERVAL) |
||||
} |
||||
} |
@ -1,19 +0,0 @@ |
||||
import blacklist from './recipient-blacklist' |
||||
|
||||
/** |
||||
* Checks if a specified account on a specified network is blacklisted |
||||
* @param {number} networkId |
||||
* @param {string} account |
||||
* @throws {Error} if the account is blacklisted on mainnet |
||||
*/ |
||||
export function throwIfAccountIsBlacklisted (networkId, account) { |
||||
const mainnetId = 1 |
||||
if (networkId !== mainnetId) { |
||||
return |
||||
} |
||||
|
||||
const accountToCheck = account.toLowerCase() |
||||
if (blacklist.includes(accountToCheck)) { |
||||
throw new Error('Recipient is a public account') |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
import blocklist from './recipient-blocklist' |
||||
|
||||
/** |
||||
* Checks if a specified account on a specified network is blocked |
||||
* @param {number} networkId |
||||
* @param {string} account |
||||
* @throws {Error} if the account is blocked on mainnet |
||||
*/ |
||||
export function throwIfAccountIsBlocked (networkId, account) { |
||||
const mainnetId = 1 |
||||
if (networkId !== mainnetId) { |
||||
return |
||||
} |
||||
|
||||
const accountToCheck = account.toLowerCase() |
||||
if (blocklist.includes(accountToCheck)) { |
||||
throw new Error('Recipient is a public account') |
||||
} |
||||
} |
After Width: | Height: | Size: 678 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,66 +0,0 @@ |
||||
import assert from 'assert' |
||||
import sinon from 'sinon' |
||||
import InfuraController from '../../../../app/scripts/controllers/infura' |
||||
|
||||
describe('infura-controller', function () { |
||||
let infuraController, networkStatus |
||||
const response = { 'mainnet': 'degraded', 'ropsten': 'ok', 'kovan': 'ok', 'rinkeby': 'down', 'goerli': 'ok' } |
||||
|
||||
describe('Network status queries', function () { |
||||
before(async function () { |
||||
infuraController = new InfuraController() |
||||
sinon.stub(infuraController, 'checkInfuraNetworkStatus').resolves(response) |
||||
networkStatus = await infuraController.checkInfuraNetworkStatus() |
||||
}) |
||||
|
||||
describe('Mainnet', function () { |
||||
it('should have Mainnet', function () { |
||||
assert.equal(Object.keys(networkStatus)[0], 'mainnet') |
||||
}) |
||||
|
||||
it('should have a value for Mainnet status', function () { |
||||
assert.equal(networkStatus.mainnet, 'degraded') |
||||
}) |
||||
}) |
||||
|
||||
describe('Ropsten', function () { |
||||
it('should have Ropsten', function () { |
||||
assert.equal(Object.keys(networkStatus)[1], 'ropsten') |
||||
}) |
||||
|
||||
it('should have a value for Ropsten status', function () { |
||||
assert.equal(networkStatus.ropsten, 'ok') |
||||
}) |
||||
}) |
||||
|
||||
describe('Kovan', function () { |
||||
it('should have Kovan', function () { |
||||
assert.equal(Object.keys(networkStatus)[2], 'kovan') |
||||
}) |
||||
|
||||
it('should have a value for Kovan status', function () { |
||||
assert.equal(networkStatus.kovan, 'ok') |
||||
}) |
||||
}) |
||||
|
||||
describe('Rinkeby', function () { |
||||
it('should have Rinkeby', function () { |
||||
assert.equal(Object.keys(networkStatus)[3], 'rinkeby') |
||||
}) |
||||
|
||||
it('should have a value for Rinkeby status', function () { |
||||
assert.equal(networkStatus.rinkeby, 'down') |
||||
}) |
||||
}) |
||||
|
||||
describe('Goerli', function () { |
||||
it('should have Goerli', function () { |
||||
assert.equal(Object.keys(networkStatus)[4], 'goerli') |
||||
}) |
||||
|
||||
it('should have a value for Goerli status', function () { |
||||
assert.equal(networkStatus.goerli, 'ok') |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -1,26 +1,29 @@ |
||||
.asset-list-item { |
||||
&__container { |
||||
display: flex; |
||||
padding: 24px 16px; |
||||
align-items: center; |
||||
border-top: 1px solid $mercury; |
||||
border-bottom: 1px solid $mercury; |
||||
cursor: pointer; |
||||
&__chevron-right { |
||||
color: $Grey-500; |
||||
} |
||||
|
||||
&:hover { |
||||
background-color: $Grey-000; |
||||
.list-item__right-content { |
||||
align-self: center; |
||||
} |
||||
|
||||
.list-item__subheading { |
||||
margin-top: 6px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&__balance { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-left: 15px; |
||||
&__warning { |
||||
flex: 1; |
||||
min-width: 0; |
||||
margin-left: 8px; |
||||
} |
||||
|
||||
&__chevron-right { |
||||
color: $Grey-500; |
||||
@media (min-width: 576px) { |
||||
&__warning-tooltip { |
||||
display: none; |
||||
} |
||||
|
||||
.list-item__mid-content { |
||||
display: flex; |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,13 +0,0 @@ |
||||
.asset-list { |
||||
&__primary-amount { |
||||
color: $Black-100; |
||||
font-size: 16px; |
||||
height: 16px; |
||||
} |
||||
|
||||
&__secondary-amount { |
||||
color: $Grey-500; |
||||
margin-top: 6px; |
||||
font-size: 14px; |
||||
} |
||||
} |
@ -1 +1 @@ |
||||
export { default } from './token-cell.container' |
||||
export { default } from './token-cell' |
||||
|
@ -1,111 +0,0 @@ |
||||
import classnames from 'classnames' |
||||
import PropTypes from 'prop-types' |
||||
import React, { Component } from 'react' |
||||
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util' |
||||
import Tooltip from '../../ui/tooltip-v2' |
||||
import { I18nContext } from '../../../contexts/i18n' |
||||
import AssetListItem from '../asset-list-item' |
||||
|
||||
export default class TokenCell extends Component { |
||||
static contextType = I18nContext |
||||
|
||||
static propTypes = { |
||||
address: PropTypes.string, |
||||
outdatedBalance: PropTypes.bool, |
||||
symbol: PropTypes.string, |
||||
string: PropTypes.string, |
||||
contractExchangeRates: PropTypes.object, |
||||
conversionRate: PropTypes.number, |
||||
currentCurrency: PropTypes.string, |
||||
image: PropTypes.string, |
||||
onClick: PropTypes.func.isRequired, |
||||
userAddress: PropTypes.string.isRequired, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
outdatedBalance: false, |
||||
} |
||||
|
||||
render () { |
||||
const t = this.context |
||||
const { |
||||
address, |
||||
symbol, |
||||
string, |
||||
contractExchangeRates, |
||||
conversionRate, |
||||
onClick, |
||||
currentCurrency, |
||||
image, |
||||
outdatedBalance, |
||||
userAddress, |
||||
} = this.props |
||||
let currentTokenToFiatRate |
||||
let currentTokenInFiat |
||||
let formattedFiat = '' |
||||
|
||||
if (contractExchangeRates[address]) { |
||||
currentTokenToFiatRate = multiplyCurrencies( |
||||
contractExchangeRates[address], |
||||
conversionRate |
||||
) |
||||
currentTokenInFiat = conversionUtil(string, { |
||||
fromNumericBase: 'dec', |
||||
fromCurrency: symbol, |
||||
toCurrency: currentCurrency.toUpperCase(), |
||||
numberOfDecimals: 2, |
||||
conversionRate: currentTokenToFiatRate, |
||||
}) |
||||
formattedFiat = currentTokenInFiat.toString() === '0' |
||||
? '' |
||||
: `${currentTokenInFiat} ${currentCurrency.toUpperCase()}` |
||||
} |
||||
|
||||
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol |
||||
|
||||
const warning = outdatedBalance |
||||
? ( |
||||
<Tooltip |
||||
interactive |
||||
position="bottom" |
||||
html={( |
||||
<div className="token-cell__outdated-tooltip"> |
||||
{ t('troubleTokenBalances') } |
||||
<a |
||||
href={`https://ethplorer.io/address/${userAddress}`} |
||||
rel="noopener noreferrer" |
||||
target="_blank" |
||||
style={{ color: '#F7861C' }} |
||||
> |
||||
{ t('here') } |
||||
</a> |
||||
</div> |
||||
)} |
||||
> |
||||
<i className={classnames(['fa', 'fa-exclamation-circle', 'token-cell__outdated-icon'])} /> |
||||
</Tooltip> |
||||
) |
||||
: null |
||||
|
||||
return ( |
||||
<AssetListItem |
||||
className={classnames('token-cell', { 'token-cell--outdated': outdatedBalance })} |
||||
iconClassName="token-cell__icon" |
||||
onClick={onClick.bind(null, address)} |
||||
tokenAddress={address} |
||||
tokenImage={image} |
||||
warning={warning} |
||||
> |
||||
<div className="token-cell__balance-wrapper"> |
||||
<div className="token-cell__token-balance">{string || 0}</div> |
||||
<div className="token-cell__token-symbol">{symbol}</div> |
||||
{showFiat && ( |
||||
<div className="token-cell__fiat-amount"> |
||||
{formattedFiat} |
||||
</div> |
||||
)} |
||||
</div> |
||||
</AssetListItem> |
||||
) |
||||
} |
||||
} |
@ -1,14 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import TokenCell from './token-cell.component' |
||||
import { getSelectedAddress } from '../../../selectors' |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
contractExchangeRates: state.metamask.contractExchangeRates, |
||||
conversionRate: state.metamask.conversionRate, |
||||
currentCurrency: state.metamask.currentCurrency, |
||||
userAddress: getSelectedAddress(state), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(TokenCell) |
@ -0,0 +1,88 @@ |
||||
import classnames from 'classnames' |
||||
import PropTypes from 'prop-types' |
||||
import React from 'react' |
||||
import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util' |
||||
import AssetListItem from '../asset-list-item' |
||||
import { useSelector } from 'react-redux' |
||||
import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, getSelectedAddress } from '../../../selectors' |
||||
import { useI18nContext } from '../../../hooks/useI18nContext' |
||||
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util' |
||||
|
||||
export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) { |
||||
const contractExchangeRates = useSelector(getTokenExchangeRates) |
||||
const conversionRate = useSelector(getConversionRate) |
||||
const currentCurrency = useSelector(getCurrentCurrency) |
||||
const userAddress = useSelector(getSelectedAddress) |
||||
const t = useI18nContext() |
||||
|
||||
let currentTokenToFiatRate |
||||
let currentTokenInFiat |
||||
let formattedFiat = '' |
||||
|
||||
|
||||
// if the conversionRate is 0 eg: currently unknown
|
||||
// or the contract exchange rate is currently unknown
|
||||
// the effective currentTokenToFiatRate is 0 and erroneous.
|
||||
// Skipping this entire block will result in fiat not being
|
||||
// shown to the user, instead of a fiat value of 0 for a non-zero
|
||||
// token amount.
|
||||
if (conversionRate > 0 && contractExchangeRates[address]) { |
||||
currentTokenToFiatRate = multiplyCurrencies( |
||||
contractExchangeRates[address], |
||||
conversionRate |
||||
) |
||||
currentTokenInFiat = conversionUtil(string, { |
||||
fromNumericBase: 'dec', |
||||
fromCurrency: symbol, |
||||
toCurrency: currentCurrency.toUpperCase(), |
||||
numberOfDecimals: 2, |
||||
conversionRate: currentTokenToFiatRate, |
||||
}) |
||||
formattedFiat = `${formatCurrency(currentTokenInFiat, currentCurrency)} ${currentCurrency.toUpperCase()}` |
||||
} |
||||
|
||||
const showFiat = Boolean(currentTokenInFiat) && currentCurrency.toUpperCase() !== symbol |
||||
|
||||
const warning = outdatedBalance |
||||
? ( |
||||
<span> |
||||
{ t('troubleTokenBalances') } |
||||
<a |
||||
href={`https://ethplorer.io/address/${userAddress}`} |
||||
rel="noopener noreferrer" |
||||
target="_blank" |
||||
style={{ color: '#F7861C' }} |
||||
> |
||||
{ t('here') } |
||||
</a> |
||||
</span> |
||||
) |
||||
: null |
||||
|
||||
return ( |
||||
<AssetListItem |
||||
className={classnames('token-cell', { 'token-cell--outdated': outdatedBalance })} |
||||
iconClassName="token-cell__icon" |
||||
onClick={onClick.bind(null, address)} |
||||
tokenAddress={address} |
||||
tokenImage={image} |
||||
warning={warning} |
||||
primary={`${string || 0} ${symbol}`} |
||||
secondary={showFiat ? formattedFiat : undefined} |
||||
/> |
||||
|
||||
) |
||||
} |
||||
|
||||
TokenCell.propTypes = { |
||||
address: PropTypes.string, |
||||
outdatedBalance: PropTypes.bool, |
||||
symbol: PropTypes.string, |
||||
string: PropTypes.string, |
||||
image: PropTypes.string, |
||||
onClick: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
TokenCell.defaultProps = { |
||||
outdatedBalance: false, |
||||
} |
@ -1,95 +1,5 @@ |
||||
$wallet-balance-breakpoint: 890px; |
||||
$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})"; |
||||
|
||||
.token-cell { |
||||
position: relative; |
||||
|
||||
&__token-balance { |
||||
margin-right: 4px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
min-width: 0; |
||||
max-width: 100%; |
||||
} |
||||
|
||||
&__token-balance, &__token-symbol { |
||||
font-size: 16px; |
||||
flex: 0 0 auto; |
||||
color: $Black-100; |
||||
} |
||||
|
||||
&__fiat-amount { |
||||
margin-top: 6px; |
||||
font-size: 14px; |
||||
width: 100%; |
||||
text-transform: uppercase; |
||||
&--outdated .list-item__heading { |
||||
color: $Grey-500; |
||||
} |
||||
|
||||
&--outdated &__icon { |
||||
opacity: 0.5 |
||||
} |
||||
&--outdated &__balance-wrapper { |
||||
opacity: 0.5 |
||||
} |
||||
|
||||
&__balance-wrapper { |
||||
flex: 1; |
||||
flex-flow: row wrap; |
||||
display: flex; |
||||
min-width: 0; |
||||
} |
||||
|
||||
&__outdated-icon { |
||||
color: $warning-yellow; |
||||
display: block; |
||||
padding: 0 10px; |
||||
} |
||||
|
||||
&__outdated-tooltip { |
||||
width: 260px; |
||||
} |
||||
} |
||||
|
||||
.token-menu-dropdown { |
||||
width: 80%; |
||||
position: absolute; |
||||
top: 52px; |
||||
right: 25px; |
||||
z-index: 2000; |
||||
|
||||
@media #{$wallet-balance-breakpoint-range} { |
||||
right: 18px; |
||||
} |
||||
|
||||
&__close-area { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
z-index: 2100; |
||||
width: 100%; |
||||
height: 100%; |
||||
cursor: default; |
||||
} |
||||
|
||||
&__container { |
||||
padding: 16px; |
||||
z-index: 2200; |
||||
position: relative; |
||||
} |
||||
|
||||
&__options { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
} |
||||
|
||||
&__option { |
||||
color: $white; |
||||
font-family: Roboto; |
||||
font-size: 16px; |
||||
line-height: 21px; |
||||
text-align: center; |
||||
} |
||||
} |
||||
|
@ -1 +1 @@ |
||||
export { default } from './token-list.container' |
||||
export { default } from './token-list' |
||||
|
@ -1,148 +0,0 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import TokenTracker from '@metamask/eth-token-tracker' |
||||
import { isEqual } from 'lodash' |
||||
import contracts from 'eth-contract-metadata' |
||||
|
||||
import { I18nContext } from '../../../contexts/i18n' |
||||
import TokenCell from '../token-cell' |
||||
|
||||
const defaultTokens = [] |
||||
for (const address in contracts) { |
||||
const contract = contracts[address] |
||||
if (contract.erc20) { |
||||
contract.address = address |
||||
defaultTokens.push(contract) |
||||
} |
||||
} |
||||
|
||||
class TokenList extends Component { |
||||
static contextType = I18nContext |
||||
|
||||
static propTypes = { |
||||
assetImages: PropTypes.object.isRequired, |
||||
network: PropTypes.string.isRequired, |
||||
onTokenClick: PropTypes.func.isRequired, |
||||
tokens: PropTypes.array.isRequired, |
||||
userAddress: PropTypes.string.isRequired, |
||||
} |
||||
|
||||
constructor () { |
||||
super() |
||||
|
||||
this.state = { |
||||
error: null, |
||||
tokensLoading: false, |
||||
tokensWithBalances: [], |
||||
} |
||||
} |
||||
|
||||
constructTokenTracker () { |
||||
const { network, tokens, userAddress } = this.props |
||||
if (!tokens || !tokens.length) { |
||||
this.setState({ |
||||
tokensLoading: false, |
||||
tokensWithBalances: [], |
||||
}) |
||||
return |
||||
} |
||||
this.setState({ tokensLoading: true }) |
||||
|
||||
if (!userAddress || network === 'loading' || !global.ethereumProvider) { |
||||
return |
||||
} |
||||
|
||||
const updateBalances = (tokensWithBalances) => { |
||||
this.setState({ |
||||
error: null, |
||||
tokensLoading: false, |
||||
tokensWithBalances, |
||||
}) |
||||
} |
||||
const showError = (error) => { |
||||
this.setState({ |
||||
error, |
||||
tokensLoading: false, |
||||
}) |
||||
} |
||||
|
||||
this.tokenTracker = new TokenTracker({ |
||||
userAddress, |
||||
provider: global.ethereumProvider, |
||||
tokens: tokens, |
||||
pollingInterval: 8000, |
||||
}) |
||||
|
||||
this.tokenTracker.on('update', updateBalances) |
||||
this.tokenTracker.on('error', showError) |
||||
this.tokenTracker.updateBalances() |
||||
} |
||||
|
||||
stopTokenTracker () { |
||||
if (this.tokenTracker) { |
||||
this.tokenTracker.stop() |
||||
this.tokenTracker.removeAllListeners('update') |
||||
this.tokenTracker.removeAllListeners('error') |
||||
} |
||||
} |
||||
|
||||
componentDidMount () { |
||||
this.constructTokenTracker() |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { network, tokens, userAddress } = this.props |
||||
if ( |
||||
isEqual(tokens, prevProps.tokens) && |
||||
userAddress === prevProps.userAddress && |
||||
network === prevProps.network |
||||
) { |
||||
return |
||||
} |
||||
this.stopTokenTracker() |
||||
this.constructTokenTracker() |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
this.stopTokenTracker() |
||||
} |
||||
|
||||
render () { |
||||
const t = this.context |
||||
const { error, tokensLoading, tokensWithBalances } = this.state |
||||
const { assetImages, network, onTokenClick } = this.props |
||||
if (network === 'loading' || tokensLoading) { |
||||
return ( |
||||
<div |
||||
style={{ |
||||
display: 'flex', |
||||
height: '250px', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
padding: '30px', |
||||
}} |
||||
> |
||||
{t('loadingTokens')} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
{tokensWithBalances.map((tokenData, index) => { |
||||
tokenData.image = assetImages[tokenData.address] |
||||
return ( |
||||
<TokenCell |
||||
key={index} |
||||
{...tokenData} |
||||
outdatedBalance={Boolean(error)} |
||||
onClick={onTokenClick} |
||||
/> |
||||
) |
||||
})} |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
|
||||
export default TokenList |
@ -1,21 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import PropTypes from 'prop-types' |
||||
import { getSelectedAddress } from '../../../selectors' |
||||
import TokenList from './token-list.component' |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
network: state.metamask.network, |
||||
tokens: state.metamask.tokens, |
||||
userAddress: getSelectedAddress(state), |
||||
assetImages: state.metamask.assetImages, |
||||
} |
||||
} |
||||
|
||||
const TokenListContainer = connect(mapStateToProps)(TokenList) |
||||
|
||||
TokenListContainer.propTypes = { |
||||
onTokenClick: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
export default TokenListContainer |
@ -0,0 +1,66 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import contracts from 'eth-contract-metadata' |
||||
import { isEqual } from 'lodash' |
||||
|
||||
import TokenCell from '../token-cell' |
||||
import { useI18nContext } from '../../../hooks/useI18nContext' |
||||
import { useTokenTracker } from '../../../hooks/useTokenTracker' |
||||
import { useSelector } from 'react-redux' |
||||
import { getAssetImages } from '../../../selectors' |
||||
import { getTokens } from '../../../ducks/metamask/metamask' |
||||
|
||||
const defaultTokens = [] |
||||
for (const address in contracts) { |
||||
const contract = contracts[address] |
||||
if (contract.erc20) { |
||||
contract.address = address |
||||
defaultTokens.push(contract) |
||||
} |
||||
} |
||||
|
||||
export default function TokenList ({ onTokenClick }) { |
||||
const t = useI18nContext() |
||||
const assetImages = useSelector(getAssetImages) |
||||
// use `isEqual` comparison function because the token array is serialized
|
||||
// from the background so it has a new reference with each background update,
|
||||
// even if the tokens haven't changed
|
||||
const tokens = useSelector(getTokens, isEqual) |
||||
const { loading, error, tokensWithBalances } = useTokenTracker(tokens) |
||||
|
||||
if (loading) { |
||||
return ( |
||||
<div |
||||
style={{ |
||||
display: 'flex', |
||||
height: '250px', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
padding: '30px', |
||||
}} |
||||
> |
||||
{t('loadingTokens')} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
{tokensWithBalances.map((tokenData, index) => { |
||||
tokenData.image = assetImages[tokenData.address] |
||||
return ( |
||||
<TokenCell |
||||
key={index} |
||||
{...tokenData} |
||||
outdatedBalance={Boolean(error)} |
||||
onClick={onTokenClick} |
||||
/> |
||||
) |
||||
})} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
TokenList.propTypes = { |
||||
onTokenClick: PropTypes.func.isRequired, |
||||
} |
@ -1 +1 @@ |
||||
export { default } from './token-balance.container' |
||||
export { default } from './token-balance' |
||||
|
@ -1,23 +0,0 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import CurrencyDisplay from '../currency-display' |
||||
|
||||
export default class TokenBalance extends PureComponent { |
||||
static propTypes = { |
||||
string: PropTypes.string, |
||||
symbol: PropTypes.string, |
||||
className: PropTypes.string, |
||||
} |
||||
|
||||
render () { |
||||
const { className, string, symbol } = this.props |
||||
|
||||
return ( |
||||
<CurrencyDisplay |
||||
className={className} |
||||
displayValue={string} |
||||
suffix={symbol} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -1,16 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'redux' |
||||
import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker' |
||||
import TokenBalance from './token-balance.component' |
||||
import { getSelectedAddress } from '../../../selectors' |
||||
|
||||
const mapStateToProps = (state) => { |
||||
return { |
||||
userAddress: getSelectedAddress(state), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
connect(mapStateToProps), |
||||
withTokenTracker |
||||
)(TokenBalance) |
@ -0,0 +1,30 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import CurrencyDisplay from '../currency-display' |
||||
import { useTokenTracker } from '../../../hooks/useTokenTracker' |
||||
|
||||
export default function TokenBalance ({ className, token }) { |
||||
const { tokensWithBalances } = useTokenTracker([token]) |
||||
|
||||
const { string, symbol } = tokensWithBalances[0] || {} |
||||
return ( |
||||
<CurrencyDisplay |
||||
className={className} |
||||
displayValue={string || ''} |
||||
suffix={symbol || ''} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
TokenBalance.propTypes = { |
||||
className: PropTypes.string, |
||||
token: PropTypes.shape({ |
||||
address: PropTypes.string.isRequired, |
||||
decimals: PropTypes.number, |
||||
symbol: PropTypes.string, |
||||
}).isRequired, |
||||
} |
||||
|
||||
TokenBalance.defaultProps = { |
||||
className: undefined, |
||||
} |
@ -1 +0,0 @@ |
||||
export { default } from './with-token-tracker.component' |
@ -1,44 +0,0 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import withTokenTracker from '../with-token-tracker.component' |
||||
import TokenBalance from '../../../../components/ui/token-balance/token-balance.component' |
||||
// import sinon from 'sinon'
|
||||
import TokenTracker from '@metamask/eth-token-tracker' |
||||
|
||||
const { createTestProviderTools } = require('../../../../../../test/stub/provider') |
||||
|
||||
const provider = createTestProviderTools({ scaffold: {} }).provider |
||||
|
||||
describe('WithTokenTracker HOC', function () { |
||||
let wrapper |
||||
|
||||
beforeEach(function () { |
||||
const TokenTracker = withTokenTracker(TokenBalance) |
||||
wrapper = shallow( |
||||
<TokenTracker |
||||
userAddress="0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" |
||||
token={ |
||||
{ |
||||
address: 'test', |
||||
} |
||||
} |
||||
/> |
||||
) |
||||
}) |
||||
|
||||
it('#setError', function () { |
||||
wrapper.instance().setError('test') |
||||
assert.equal(wrapper.props().error, 'test') |
||||
}) |
||||
|
||||
it('#updateBalance', function () { |
||||
wrapper.instance().tracker = new TokenTracker({ |
||||
provider, |
||||
}) |
||||
wrapper.instance().updateBalance([{ string: 'test string', symbol: 'test symbol' }]) |
||||
assert.equal(wrapper.props().string, 'test string') |
||||
assert.equal(wrapper.props().symbol, 'test symbol') |
||||
}) |
||||
|
||||
}) |
@ -1,101 +0,0 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import TokenTracker from '@metamask/eth-token-tracker' |
||||
|
||||
export default function withTokenTracker (WrappedComponent) { |
||||
return class TokenTrackerWrappedComponent extends Component { |
||||
static propTypes = { |
||||
userAddress: PropTypes.string.isRequired, |
||||
token: PropTypes.object.isRequired, |
||||
} |
||||
|
||||
state = { |
||||
string: '', |
||||
symbol: '', |
||||
balance: '', |
||||
error: null, |
||||
} |
||||
|
||||
tracker = null |
||||
|
||||
componentDidMount () { |
||||
this.createFreshTokenTracker() |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props |
||||
const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps |
||||
|
||||
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) { |
||||
return |
||||
} |
||||
|
||||
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) { |
||||
return |
||||
} |
||||
|
||||
this.createFreshTokenTracker() |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
this.removeListeners() |
||||
} |
||||
|
||||
createFreshTokenTracker () { |
||||
this.removeListeners() |
||||
|
||||
if (!global.ethereumProvider) { |
||||
return |
||||
} |
||||
|
||||
const { userAddress, token } = this.props |
||||
|
||||
this.tracker = new TokenTracker({ |
||||
userAddress, |
||||
provider: global.ethereumProvider, |
||||
tokens: [token], |
||||
pollingInterval: 8000, |
||||
}) |
||||
|
||||
this.tracker.on('update', this.updateBalance) |
||||
this.tracker.on('error', this.setError) |
||||
|
||||
this.tracker.updateBalances() |
||||
.then(() => this.updateBalance(this.tracker.serialize())) |
||||
.catch((error) => this.setState({ error: error.message })) |
||||
} |
||||
|
||||
setError = (error) => { |
||||
this.setState({ error }) |
||||
} |
||||
|
||||
updateBalance = (tokens = []) => { |
||||
if (!this.tracker.running) { |
||||
return |
||||
} |
||||
const [{ string, symbol, balance }] = tokens |
||||
this.setState({ string, symbol, error: null, balance }) |
||||
} |
||||
|
||||
removeListeners () { |
||||
if (this.tracker) { |
||||
this.tracker.stop() |
||||
this.tracker.removeListener('update', this.updateBalance) |
||||
this.tracker.removeListener('error', this.setError) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { balance, string, symbol, error } = this.state |
||||
return ( |
||||
<WrappedComponent |
||||
{ ...this.props } |
||||
string={string} |
||||
symbol={symbol} |
||||
tokenTrackerBalance={balance} |
||||
error={error} |
||||
/> |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,88 @@ |
||||
import { useState, useEffect, useRef, useCallback } from 'react' |
||||
import TokenTracker from '@metamask/eth-token-tracker' |
||||
import { useSelector } from 'react-redux' |
||||
import { getCurrentNetwork, getSelectedAddress } from '../selectors' |
||||
|
||||
|
||||
export function useTokenTracker (tokens) { |
||||
const network = useSelector(getCurrentNetwork) |
||||
const userAddress = useSelector(getSelectedAddress) |
||||
|
||||
const [loading, setLoading] = useState(() => tokens?.length >= 0) |
||||
const [tokensWithBalances, setTokensWithBalances] = useState([]) |
||||
const [error, setError] = useState(null) |
||||
const tokenTracker = useRef(null) |
||||
|
||||
const updateBalances = useCallback((tokensWithBalances) => { |
||||
setTokensWithBalances(tokensWithBalances) |
||||
setLoading(false) |
||||
setError(null) |
||||
}, []) |
||||
|
||||
const showError = useCallback((error) => { |
||||
setError(error) |
||||
setLoading(false) |
||||
}, []) |
||||
|
||||
const teardownTracker = useCallback(() => { |
||||
if (tokenTracker.current) { |
||||
tokenTracker.current.stop() |
||||
tokenTracker.current.removeAllListeners('update') |
||||
tokenTracker.current.removeAllListeners('error') |
||||
tokenTracker.current = null |
||||
} |
||||
}, []) |
||||
|
||||
const buildTracker = useCallback((address, tokenList) => { |
||||
// clear out previous tracker, if it exists.
|
||||
teardownTracker() |
||||
tokenTracker.current = new TokenTracker({ |
||||
userAddress: address, |
||||
provider: global.ethereumProvider, |
||||
tokens: tokenList, |
||||
pollingInterval: 8000, |
||||
}) |
||||
|
||||
tokenTracker.current.on('update', updateBalances) |
||||
tokenTracker.current.on('error', showError) |
||||
tokenTracker.current.updateBalances() |
||||
}, [updateBalances, showError, teardownTracker]) |
||||
|
||||
// Effect to remove the tracker when the component is removed from DOM
|
||||
// Do not overload this effect with additional dependencies. teardownTracker
|
||||
// is the only dependency here, which itself has no dependencies and will
|
||||
// never update. The lack of dependencies that change is what confirms
|
||||
// that this effect only runs on mount/unmount
|
||||
useEffect(() => { |
||||
return teardownTracker |
||||
}, [teardownTracker]) |
||||
|
||||
|
||||
// Effect to set loading state and initialize tracker when values change
|
||||
useEffect(() => { |
||||
// This effect will only run initially and when:
|
||||
// 1. network is updated,
|
||||
// 2. userAddress is changed,
|
||||
// 3. token list is updated and not equal to previous list
|
||||
// in any of these scenarios, we should indicate to the user that their token
|
||||
// values are in the process of updating by setting loading state.
|
||||
setLoading(true) |
||||
|
||||
if (!userAddress || network === 'loading' || !global.ethereumProvider) { |
||||
// If we do not have enough information to build a TokenTracker, we exit early
|
||||
// When the values above change, the effect will be restarted. We also teardown
|
||||
// tracker because inevitably this effect will run again momentarily.
|
||||
teardownTracker() |
||||
return |
||||
} |
||||
|
||||
if (tokens.length === 0) { |
||||
// sets loading state to false and token list to empty
|
||||
updateBalances([]) |
||||
} |
||||
|
||||
buildTracker(userAddress, tokens) |
||||
}, [userAddress, network, tokens, updateBalances, buildTracker]) |
||||
|
||||
return { loading, tokensWithBalances, error } |
||||
} |
@ -1,115 +0,0 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
import ConfirmApproveContent from './confirm-approve-content' |
||||
import { getCustomTxParamsData } from './confirm-approve.util' |
||||
import { |
||||
calcTokenAmount, |
||||
} from '../../helpers/utils/token-util' |
||||
|
||||
export default class ConfirmApprove extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
tokenAddress: PropTypes.string, |
||||
toAddress: PropTypes.string, |
||||
tokenAmount: PropTypes.string, |
||||
tokenSymbol: PropTypes.string, |
||||
fiatTransactionTotal: PropTypes.string, |
||||
ethTransactionTotal: PropTypes.string, |
||||
contractExchangeRate: PropTypes.number, |
||||
conversionRate: PropTypes.number, |
||||
currentCurrency: PropTypes.string, |
||||
showCustomizeGasModal: PropTypes.func, |
||||
showEditApprovalPermissionModal: PropTypes.func, |
||||
origin: PropTypes.string, |
||||
siteImage: PropTypes.string, |
||||
tokenTrackerBalance: PropTypes.string, |
||||
data: PropTypes.string, |
||||
decimals: PropTypes.number, |
||||
txData: PropTypes.object, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
tokenAmount: '0', |
||||
} |
||||
|
||||
state = { |
||||
customPermissionAmount: '', |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { tokenAmount } = this.props |
||||
|
||||
if (tokenAmount !== prevProps.tokenAmount) { |
||||
this.setState({ customPermissionAmount: tokenAmount }) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
toAddress, |
||||
tokenAddress, |
||||
tokenSymbol, |
||||
tokenAmount, |
||||
showCustomizeGasModal, |
||||
showEditApprovalPermissionModal, |
||||
origin, |
||||
siteImage, |
||||
tokenTrackerBalance, |
||||
data, |
||||
decimals, |
||||
txData, |
||||
currentCurrency, |
||||
ethTransactionTotal, |
||||
fiatTransactionTotal, |
||||
...restProps |
||||
} = this.props |
||||
const { customPermissionAmount } = this.state |
||||
|
||||
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}` |
||||
|
||||
const tokenBalance = tokenTrackerBalance |
||||
? calcTokenAmount(tokenTrackerBalance, decimals).toString(10) |
||||
: '' |
||||
|
||||
const customData = customPermissionAmount |
||||
? getCustomTxParamsData(data, { customPermissionAmount, decimals }) |
||||
: null |
||||
|
||||
return ( |
||||
<ConfirmTransactionBase |
||||
toAddress={toAddress} |
||||
identiconAddress={tokenAddress} |
||||
showAccountInHeader |
||||
title={tokensText} |
||||
contentComponent={( |
||||
<ConfirmApproveContent |
||||
decimals={decimals} |
||||
siteImage={siteImage} |
||||
setCustomAmount={(newAmount) => { |
||||
this.setState({ customPermissionAmount: newAmount }) |
||||
}} |
||||
customTokenAmount={String(customPermissionAmount)} |
||||
tokenAmount={tokenAmount} |
||||
origin={origin} |
||||
tokenSymbol={tokenSymbol} |
||||
tokenBalance={tokenBalance} |
||||
showCustomizeGasModal={() => showCustomizeGasModal(txData)} |
||||
showEditApprovalPermissionModal={showEditApprovalPermissionModal} |
||||
data={customData || data} |
||||
toAddress={toAddress} |
||||
currentCurrency={currentCurrency} |
||||
ethTransactionTotal={ethTransactionTotal} |
||||
fiatTransactionTotal={fiatTransactionTotal} |
||||
/> |
||||
)} |
||||
hideSenderToRecipient |
||||
customTxParamsData={customData} |
||||
{...restProps} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -1,113 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { |
||||
contractExchangeRateSelector, |
||||
transactionFeeSelector, |
||||
} from '../../selectors' |
||||
import { getTokens } from '../../ducks/metamask/metamask' |
||||
import { showModal } from '../../store/actions' |
||||
import { |
||||
getTokenData, |
||||
} from '../../helpers/utils/transactions.util' |
||||
import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker' |
||||
import { |
||||
calcTokenAmount, |
||||
getTokenToAddress, |
||||
getTokenValue, |
||||
} from '../../helpers/utils/token-util' |
||||
import ConfirmApprove from './confirm-approve.component' |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { match: { params = {} } } = ownProps |
||||
const { id: paramsTransactionId } = params |
||||
const { |
||||
confirmTransaction, |
||||
metamask: { |
||||
currentCurrency, |
||||
conversionRate, |
||||
currentNetworkTxList, |
||||
domainMetadata = {}, |
||||
selectedAddress, |
||||
}, |
||||
} = state |
||||
|
||||
const { |
||||
txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {}, |
||||
} = confirmTransaction |
||||
|
||||
const transaction = ( |
||||
currentNetworkTxList.find(({ id }) => id === (Number(paramsTransactionId) || |
||||
transactionId)) || {} |
||||
) |
||||
|
||||
const { |
||||
ethTransactionTotal, |
||||
fiatTransactionTotal, |
||||
} = transactionFeeSelector(state, transaction) |
||||
const tokens = getTokens(state) |
||||
const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address) |
||||
const { decimals, symbol: tokenSymbol } = currentToken || {} |
||||
|
||||
const tokenData = getTokenData(data) |
||||
const tokenValue = tokenData && getTokenValue(tokenData.params) |
||||
const toAddress = tokenData && getTokenToAddress(tokenData.params) |
||||
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toString(10) |
||||
const contractExchangeRate = contractExchangeRateSelector(state) |
||||
|
||||
const { origin } = transaction |
||||
const formattedOrigin = origin |
||||
? origin[0].toUpperCase() + origin.slice(1) |
||||
: '' |
||||
|
||||
const { icon: siteImage = '' } = domainMetadata[origin] || {} |
||||
return { |
||||
toAddress, |
||||
tokenAddress, |
||||
tokenAmount, |
||||
currentCurrency, |
||||
conversionRate, |
||||
contractExchangeRate, |
||||
fiatTransactionTotal, |
||||
ethTransactionTotal, |
||||
tokenSymbol, |
||||
siteImage, |
||||
token: { address: tokenAddress }, |
||||
userAddress: selectedAddress, |
||||
origin: formattedOrigin, |
||||
data, |
||||
decimals: Number(decimals), |
||||
txData: transaction, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = (dispatch) => { |
||||
return { |
||||
showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })), |
||||
showEditApprovalPermissionModal: ({ |
||||
customTokenAmount, |
||||
decimals, |
||||
origin, |
||||
setCustomAmount, |
||||
tokenAmount, |
||||
tokenBalance, |
||||
tokenSymbol, |
||||
}) => dispatch(showModal({ |
||||
name: 'EDIT_APPROVAL_PERMISSION', |
||||
customTokenAmount, |
||||
decimals, |
||||
origin, |
||||
setCustomAmount, |
||||
tokenAmount, |
||||
tokenBalance, |
||||
tokenSymbol, |
||||
})), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps), |
||||
withTokenTracker, |
||||
)(ConfirmApprove) |
||||
|
@ -0,0 +1,141 @@ |
||||
import React, { useEffect, useRef, useState } from 'react' |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { useParams } from 'react-router-dom' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
import ConfirmApproveContent from './confirm-approve-content' |
||||
import { getCustomTxParamsData } from './confirm-approve.util' |
||||
import { showModal } from '../../store/actions' |
||||
import { |
||||
getTokenData, |
||||
} from '../../helpers/utils/transactions.util' |
||||
import { |
||||
calcTokenAmount, |
||||
getTokenToAddress, |
||||
getTokenValue, |
||||
} from '../../helpers/utils/token-util' |
||||
import { useTokenTracker } from '../../hooks/useTokenTracker' |
||||
import { getTokens } from '../../ducks/metamask/metamask' |
||||
import { |
||||
transactionFeeSelector, |
||||
txDataSelector, |
||||
} from '../../selectors/confirm-transaction' |
||||
import { getCurrentCurrency, getDomainMetadata } from '../../selectors/selectors' |
||||
import { currentNetworkTxListSelector } from '../../selectors/transactions' |
||||
|
||||
export default function ConfirmApprove () { |
||||
const dispatch = useDispatch() |
||||
const { id: paramsTransactionId } = useParams() |
||||
const { |
||||
id: transactionId, |
||||
txParams: { |
||||
to: tokenAddress, |
||||
data, |
||||
} = {}, |
||||
} = useSelector(txDataSelector) |
||||
|
||||
const currentCurrency = useSelector(getCurrentCurrency) |
||||
const currentNetworkTxList = useSelector(currentNetworkTxListSelector) |
||||
const domainMetadata = useSelector(getDomainMetadata) |
||||
const tokens = useSelector(getTokens) |
||||
|
||||
const transaction = ( |
||||
currentNetworkTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {} |
||||
) |
||||
const { |
||||
ethTransactionTotal, |
||||
fiatTransactionTotal, |
||||
} = useSelector((state) => transactionFeeSelector(state, transaction)) |
||||
|
||||
const currentToken = (tokens && tokens.find(({ address }) => tokenAddress === address)) || { address: tokenAddress } |
||||
|
||||
const { tokensWithBalances } = useTokenTracker([currentToken]) |
||||
const tokenTrackerBalance = tokensWithBalances[0]?.balance || '' |
||||
|
||||
const tokenSymbol = currentToken?.symbol |
||||
const decimals = Number(currentToken?.decimals) |
||||
const tokenData = getTokenData(data) |
||||
const tokenValue = tokenData && getTokenValue(tokenData.params) |
||||
const toAddress = tokenData && getTokenToAddress(tokenData.params) |
||||
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toString(10) |
||||
|
||||
const [customPermissionAmount, setCustomPermissionAmount] = useState('') |
||||
|
||||
const previousTokenAmount = useRef(tokenAmount) |
||||
|
||||
useEffect( |
||||
() => { |
||||
if (customPermissionAmount && previousTokenAmount.current !== tokenAmount) { |
||||
setCustomPermissionAmount(tokenAmount) |
||||
} |
||||
previousTokenAmount.current = tokenAmount |
||||
}, |
||||
[customPermissionAmount, tokenAmount] |
||||
) |
||||
|
||||
const { origin } = transaction |
||||
const formattedOrigin = origin |
||||
? origin[0].toUpperCase() + origin.slice(1) |
||||
: '' |
||||
const txData = transaction |
||||
|
||||
const { icon: siteImage = '' } = domainMetadata[origin] || {} |
||||
|
||||
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}` |
||||
const tokenBalance = tokenTrackerBalance |
||||
? calcTokenAmount(tokenTrackerBalance, decimals).toString(10) |
||||
: '' |
||||
const customData = customPermissionAmount |
||||
? getCustomTxParamsData(data, { customPermissionAmount, decimals }) |
||||
: null |
||||
|
||||
return ( |
||||
<ConfirmTransactionBase |
||||
toAddress={toAddress} |
||||
identiconAddress={tokenAddress} |
||||
showAccountInHeader |
||||
title={tokensText} |
||||
contentComponent={( |
||||
<ConfirmApproveContent |
||||
decimals={decimals} |
||||
siteImage={siteImage} |
||||
setCustomAmount={setCustomPermissionAmount} |
||||
customTokenAmount={String(customPermissionAmount)} |
||||
tokenAmount={tokenAmount} |
||||
origin={formattedOrigin} |
||||
tokenSymbol={tokenSymbol} |
||||
tokenBalance={tokenBalance} |
||||
showCustomizeGasModal={() => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData }))} |
||||
showEditApprovalPermissionModal={ |
||||
({ |
||||
customTokenAmount, |
||||
decimals, |
||||
origin, |
||||
setCustomAmount, |
||||
tokenAmount, |
||||
tokenBalance, |
||||
tokenSymbol, |
||||
}) => dispatch( |
||||
showModal({ |
||||
name: 'EDIT_APPROVAL_PERMISSION', |
||||
customTokenAmount, |
||||
decimals, |
||||
origin, |
||||
setCustomAmount, |
||||
tokenAmount, |
||||
tokenBalance, |
||||
tokenSymbol, |
||||
}) |
||||
) |
||||
} |
||||
data={customData || data} |
||||
toAddress={toAddress} |
||||
currentCurrency={currentCurrency} |
||||
ethTransactionTotal={ethTransactionTotal} |
||||
fiatTransactionTotal={fiatTransactionTotal} |
||||
/> |
||||
)} |
||||
hideSenderToRecipient |
||||
customTxParamsData={customData} |
||||
/> |
||||
) |
||||
} |
@ -1 +1 @@ |
||||
export { default } from './confirm-approve.container' |
||||
export { default } from './confirm-approve' |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue