1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

add erc-721 token detection and flag to disable sending (#11210)

* add erc-721 token detection and flag to disable sending

* addressing feedback

* remove redundant provider instantiation

* fix issue caused by unprotected destructuring

* add tests and documentation

* move add isERC721 flag to useTokenTracker hook

* Update  and  unit tests

* use memoizedTokens in useTokenTracker

Co-authored-by: Dan Miller <danjm.com@gmail.com>
This commit is contained in:
Alex Donesky 2021-06-22 12:39:44 -05:00 committed by ryanml
parent bd38b02d8e
commit 85f17831a2
16 changed files with 348 additions and 52 deletions

View File

@ -2369,6 +2369,10 @@
"message": "verify the network details",
"description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key."
},
"unsendableAsset": {
"message": "Sending collectible (ERC-721) tokens is not currently supported",
"description": "This is an error message we show the user if they attempt to send a collectible asset type, for which currently don't support sending"
},
"updatedWithDate": {
"message": "Updated $1"
},

View File

@ -11,7 +11,7 @@ import PreferencesController from './preferences';
describe('DetectTokensController', function () {
const sandbox = sinon.createSandbox();
let keyringMemStore, network, preferences;
let keyringMemStore, network, preferences, provider;
const noop = () => undefined;
@ -23,12 +23,16 @@ describe('DetectTokensController', function () {
keyringMemStore = new ObservableStore({ isUnlocked: false });
network = new NetworkController();
network.setInfuraProjectId('foo');
preferences = new PreferencesController({ network });
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
preferences = new PreferencesController({ network, provider });
preferences.setAddresses([
'0x7e57e2',
'0xbc86727e770de68b1060c91f6bb6945c73e10388',
]);
network.initializeProvider(networkControllerProviderConfig);
sandbox
.stub(preferences, '_detectIsERC721')
.returns(Promise.resolve(false));
});
after(function () {
@ -125,6 +129,7 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
isERC721: false,
},
]);
});
@ -177,11 +182,13 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
isERC721: false,
},
{
address: tokenAddressToAdd.toLowerCase(),
decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol,
isERC721: false,
},
]);
});
@ -234,11 +241,13 @@ describe('DetectTokensController', function () {
address: existingTokenAddress.toLowerCase(),
decimals: existingToken.decimals,
symbol: existingToken.symbol,
isERC721: false,
},
{
address: tokenAddressToAdd.toLowerCase(),
decimals: tokenToAdd.decimals,
symbol: tokenToAdd.symbol,
isERC721: false,
},
]);
});

View File

@ -2,14 +2,21 @@ import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import { normalize as normalizeAddress } from 'eth-sig-util';
import ethers from 'ethers';
import { ethers } from 'ethers';
import log from 'loglevel';
import abiERC721 from 'human-standard-collectible-abi';
import contractsMap from '@metamask/contract-metadata';
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens';
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { isValidHexAddress } from '../../../shared/modules/hexstring-utils';
import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { NETWORK_EVENTS } from './network';
const ERC721METADATA_INTERFACE_ID = '0x5b5e139f';
export default class PreferencesController {
/**
*
@ -73,11 +80,18 @@ export default class PreferencesController {
};
this.network = opts.network;
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this.store = new ObservableStore(initState);
this.store.setMaxListeners(12);
this.openPopup = opts.openPopup;
this.migrateAddressBookState = opts.migrateAddressBookState;
this._subscribeToNetworkDidChange();
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this.ethersProvider = new ethers.providers.Web3Provider(opts.provider);
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
this._subscribeToInfuraAvailability();
global.setPreference = (key, value) => {
@ -393,6 +407,8 @@ export default class PreferencesController {
});
const previousIndex = tokens.indexOf(previousEntry);
newEntry.isERC721 = await this._detectIsERC721(newEntry.address);
if (previousEntry) {
tokens[previousIndex] = newEntry;
} else {
@ -403,6 +419,24 @@ export default class PreferencesController {
return Promise.resolve(tokens);
}
/**
* Adds isERC721 field to token object
* (Called when a user attempts to add tokens that were previously added which do not yet had isERC721 field)
*
* @param {string} tokenAddress - The contract address of the token requiring the isERC721 field added.
* @returns {Promise<object>} The new token object with the added isERC721 field.
*
*/
async updateTokenType(tokenAddress) {
const { tokens } = this.store.getState();
const tokenIndex = tokens.findIndex((token) => {
return token.address === tokenAddress;
});
tokens[tokenIndex].isERC721 = await this._detectIsERC721(tokenAddress);
this.store.updateState({ tokens });
return Promise.resolve(tokens[tokenIndex]);
}
/**
* Removes a specified token from the tokens array and adds it to hiddenTokens array
*
@ -480,11 +514,8 @@ export default class PreferencesController {
let addressBookKey = rpcDetail.chainId;
if (!addressBookKey) {
// We need to find the networkId to determine what these addresses were keyed by
const provider = new ethers.providers.JsonRpcProvider(
rpcDetail.rpcUrl,
);
try {
addressBookKey = await provider.send('net_version');
addressBookKey = await this.ethersProvider.send('net_version');
assert(typeof addressBookKey === 'string');
} catch (error) {
log.debug(error);
@ -701,17 +732,6 @@ export default class PreferencesController {
// PRIVATE METHODS
//
/**
* Handle updating token list to reflect current network by listening for the
* NETWORK_DID_CHANGE event.
*/
_subscribeToNetworkDidChange() {
this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const { tokens, hiddenTokens } = this._getTokenRelatedStates();
this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens);
});
}
_subscribeToInfuraAvailability() {
this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => {
this._setInfuraBlocked(true);
@ -763,6 +783,43 @@ export default class PreferencesController {
});
}
/**
* Detects whether or not a token is ERC-721 compatible.
*
* @param {string} tokensAddress - the token contract address.
*
*/
async _detectIsERC721(tokenAddress) {
const checksumAddress = toChecksumHexAddress(tokenAddress);
// if this token is already in our contract metadata map we don't need
// to check against the contract
if (contractsMap[checksumAddress]?.erc721 === true) {
return Promise.resolve(true);
}
const tokenContract = await this._createEthersContract(
tokenAddress,
abiERC721,
this.ethersProvider,
);
return await tokenContract
.supportsInterface(ERC721METADATA_INTERFACE_ID)
.catch((error) => {
console.log('error', error);
log.debug(error);
return false;
});
}
async _createEthersContract(tokenAddress, abi, ethersProvider) {
const tokenContract = await new ethers.Contract(
tokenAddress,
abi,
ethersProvider,
);
return tokenContract;
}
/**
* Updates `tokens` and `hiddenTokens` of current account and network.
*

View File

@ -1,10 +1,13 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import contractMaps from '@metamask/contract-metadata';
import abiERC721 from 'human-standard-collectible-abi';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';
describe('preferences controller', function () {
let preferencesController;
@ -13,19 +16,32 @@ describe('preferences controller', function () {
let triggerNetworkChange;
let switchToMainnet;
let switchToRinkeby;
let provider;
const migrateAddressBookState = sinon.stub();
beforeEach(function () {
const sandbox = sinon.createSandbox();
currentChainId = MAINNET_CHAIN_ID;
network = {
getCurrentChainId: () => currentChainId,
on: sinon.spy(),
const networkControllerProviderConfig = {
getAccounts: () => undefined,
};
network = new NetworkController();
network.setInfuraProjectId('foo');
network.initializeProvider(networkControllerProviderConfig);
provider = network.getProviderAndBlockTracker().provider;
sandbox.stub(network, 'getCurrentChainId').callsFake(() => currentChainId);
sandbox
.stub(network, 'getProviderConfig')
.callsFake(() => ({ type: 'mainnet' }));
const spy = sandbox.spy(network, 'on');
preferencesController = new PreferencesController({
migrateAddressBookState,
network,
provider,
});
triggerNetworkChange = network.on.firstCall.args[1];
triggerNetworkChange = spy.firstCall.args[1];
switchToMainnet = () => {
currentChainId = MAINNET_CHAIN_ID;
triggerNetworkChange();
@ -86,6 +102,104 @@ describe('preferences controller', function () {
});
});
describe('updateTokenType', function () {
it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () {
const contractAddresses = Object.keys(contractMaps);
const erc721ContractAddresses = contractAddresses.filter(
(contractAddress) => contractMaps[contractAddress].erc721 === true,
);
const address = erc721ContractAddresses[0];
const { symbol, decimals } = contractMaps[address];
preferencesController.store.updateState({
tokens: [{ address, symbol, decimals }],
});
const result = await preferencesController.updateTokenType(address);
assert.equal(result.isERC721, true);
});
it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
preferencesController.store.updateState({
tokens: [
{
address: tokenAddress,
symbol: 'TESTNFT',
decimals: '0',
},
],
});
sinon
.stub(preferencesController, '_detectIsERC721')
.callsFake(() => true);
const result = await preferencesController.updateTokenType(tokenAddress);
assert.equal(
preferencesController._detectIsERC721.getCall(0).args[0],
tokenAddress,
);
assert.equal(result.isERC721, true);
});
});
describe('_detectIsERC721', function () {
it('should return true when token is in our contract-metadata repo', async function () {
const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d';
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(result, true);
});
it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, true);
});
it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () {
const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d';
const supportsInterfaceStub = sinon
.stub()
.returns(Promise.resolve(false));
sinon
.stub(preferencesController, '_createEthersContract')
.callsFake(() => ({ supportsInterface: supportsInterfaceStub }));
const result = await preferencesController._detectIsERC721(tokenAddress);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[0],
tokenAddress,
);
assert.deepEqual(
preferencesController._createEthersContract.getCall(0).args[1],
abiERC721,
);
assert.equal(
preferencesController._createEthersContract.getCall(0).args[2],
preferencesController.ethersProvider,
);
assert.equal(result, false);
});
});
describe('removeAddress', function () {
it('should remove an address from state', function () {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
@ -291,7 +405,12 @@ describe('preferences controller', function () {
assert.equal(tokens.length, 1, 'one token removed');
const [token1] = tokens;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
});
it('should remove a token from its state on corresponding address', async function () {
@ -310,7 +429,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in account');
const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
await preferencesController.setSelectedAddress('0x7e57e3');
const tokensSecond = preferencesController.getTokens();
@ -335,7 +459,12 @@ describe('preferences controller', function () {
assert.equal(tokensFirst.length, 1, 'one token removed in network');
const [token1] = tokensFirst;
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 });
assert.deepEqual(token1, {
address: '0xb',
symbol: 'B',
decimals: 5,
isERC721: false,
});
switchToRinkeby();
const tokensSecond = preferencesController.getTokens();

View File

@ -132,11 +132,17 @@ export default class MetamaskController extends EventEmitter {
this.networkController = new NetworkController(initState.NetworkController);
this.networkController.setInfuraProjectId(opts.infuraProjectId);
// now we can initialize the RPC provider, which other controllers require
this.initializeProvider();
this.provider = this.networkController.getProviderAndBlockTracker().provider;
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
openPopup: opts.openPopup,
network: this.networkController,
provider: this.provider,
migrateAddressBookState: this.migrateAddressBookState.bind(this),
});
@ -183,11 +189,6 @@ export default class MetamaskController extends EventEmitter {
initState.NotificationController,
);
// now we can initialize the RPC provider, which other controllers require
this.initializeProvider();
this.provider = this.networkController.getProviderAndBlockTracker().provider;
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker;
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({
preferences: this.preferencesController.store,
@ -727,6 +728,10 @@ export default class MetamaskController extends EventEmitter {
preferencesController,
),
addToken: nodeify(preferencesController.addToken, preferencesController),
updateTokenType: nodeify(
preferencesController.updateTokenType,
preferencesController,
),
removeToken: nodeify(
preferencesController.removeToken,
preferencesController,

View File

@ -151,6 +151,7 @@
"fast-safe-stringify": "^2.0.7",
"fuse.js": "^3.2.0",
"globalthis": "^1.0.1",
"human-standard-collectible-abi": "^1.0.2",
"human-standard-token-abi": "^2.0.0",
"immer": "^8.0.1",
"json-rpc-engine": "^6.1.0",

View File

@ -27,6 +27,7 @@ const AssetListItem = ({
primary,
secondary,
identiconBorder,
isERC721,
}) => {
const t = useI18nContext();
const dispatch = useDispatch();
@ -121,10 +122,12 @@ const AssetListItem = ({
}
midContent={midContent}
rightContent={
<>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
{sendTokenButton}
</>
!isERC721 && (
<>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
{sendTokenButton}
</>
)
}
/>
);
@ -143,6 +146,7 @@ AssetListItem.propTypes = {
'primary': PropTypes.string,
'secondary': PropTypes.string,
'identiconBorder': PropTypes.bool,
'isERC721': PropTypes.bool,
};
AssetListItem.defaultProps = {

View File

@ -15,6 +15,7 @@ export default function TokenCell({
string,
image,
onClick,
isERC721,
}) {
const userAddress = useSelector(getSelectedAddress);
const t = useI18nContext();
@ -50,6 +51,7 @@ export default function TokenCell({
warning={warning}
primary={`${string || 0}`}
secondary={formattedFiat}
isERC721={isERC721}
/>
);
}
@ -62,6 +64,7 @@ TokenCell.propTypes = {
string: PropTypes.string,
image: PropTypes.string,
onClick: PropTypes.func.isRequired,
isERC721: PropTypes.bool,
};
TokenCell.defaultProps = {

View File

@ -91,6 +91,7 @@ const TokenOverview = ({ className, token }) => {
Icon={SendIcon}
label={t('send')}
data-testid="eth-overview-send"
disabled={token.isERC721}
/>
<IconButton
className="token-overview__button"
@ -145,6 +146,7 @@ TokenOverview.propTypes = {
address: PropTypes.string.isRequired,
decimals: PropTypes.number,
symbol: PropTypes.string,
isERC721: PropTypes.bool,
}).isRequired,
};

View File

@ -5,3 +5,4 @@ export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract';
export const ETH_GAS_PRICE_FETCH_WARNING_KEY = 'ethGasPriceFetchWarning';
export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
export const UNSENDABLE_ASSET_ERROR_KEY = 'unsendableAsset';

View File

@ -23,11 +23,19 @@ export function useTokenTracker(
const matchingTokens = hideZeroBalanceTokens
? tokenWithBalances.filter((token) => Number(token.balance) > 0)
: tokenWithBalances;
setTokensWithBalances(matchingTokens);
// TODO: improve this pattern for adding this field when we improve support for
// EIP721 tokens.
const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => {
const additionalTokenData = memoizedTokens.find(
(t) => t.address === token.address,
);
return { ...token, isERC721: additionalTokenData?.isERC721 };
});
setTokensWithBalances(matchingTokensWithIsERC721Flag);
setLoading(false);
setError(null);
},
[hideZeroBalanceTokens],
[hideZeroBalanceTokens, memoizedTokens],
);
const showError = useCallback((err) => {

View File

@ -22,6 +22,9 @@ export default class SendAssetRow extends Component {
setSendToken: PropTypes.func.isRequired,
nativeCurrency: PropTypes.string,
nativeCurrencyImage: PropTypes.string,
setUnsendableAssetError: PropTypes.func.isRequired,
updateSendErrors: PropTypes.func.isRequired,
updateTokenType: PropTypes.func.isRequired,
};
static contextTypes = {
@ -31,13 +34,41 @@ export default class SendAssetRow extends Component {
state = {
isShowingDropdown: false,
sendableTokens: [],
};
async componentDidMount() {
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
this.setState({ sendableTokens });
}
openDropdown = () => this.setState({ isShowingDropdown: true });
closeDropdown = () => this.setState({ isShowingDropdown: false });
selectToken = (token) => {
clearUnsendableAssetError = () => {
this.props.setUnsendableAssetError(false);
this.props.updateSendErrors({
unsendableAssetError: null,
gasLoadingError: null,
});
};
selectToken = async (token) => {
if (token && token.isERC721 === undefined) {
const updatedToken = await this.props.updateTokenType(token.address);
if (updatedToken.isERC721) {
this.props.setUnsendableAssetError(true);
this.props.updateSendErrors({
unsendableAssetError: 'unsendableAssetError',
});
}
}
if ((token && token.isERC721 === false) || token === undefined) {
this.clearUnsendableAssetError();
}
this.setState(
{
isShowingDropdown: false,
@ -65,7 +96,9 @@ export default class SendAssetRow extends Component {
<SendRowWrapper label={`${t('asset')}:`}>
<div className="send-v2__asset-dropdown">
{this.renderSendToken()}
{this.props.tokens.length > 0 ? this.renderAssetDropdown() : null}
{this.state.sendableTokens.length > 0
? this.renderAssetDropdown()
: null}
</div>
</SendRowWrapper>
);
@ -96,7 +129,9 @@ export default class SendAssetRow extends Component {
/>
<div className="send-v2__asset-dropdown__list">
{this.renderNativeCurrency(true)}
{this.props.tokens.map((token) => this.renderAsset(token, true))}
{this.state.sendableTokens.map((token) =>
this.renderAsset(token, true),
)}
</div>
</div>
)
@ -119,7 +154,7 @@ export default class SendAssetRow extends Component {
return (
<div
className={
this.props.tokens.length > 0
this.state.sendableTokens.length > 0
? 'send-v2__asset-dropdown__asset'
: 'send-v2__asset-dropdown__single-asset'
}
@ -146,7 +181,7 @@ export default class SendAssetRow extends Component {
/>
</div>
</div>
{!insideDropdown && this.props.tokens.length > 0 && (
{!insideDropdown && this.state.sendableTokens.length > 0 && (
<i className="fa fa-caret-down fa-lg send-v2__asset-dropdown__caret" />
)}
</div>

View File

@ -6,7 +6,11 @@ import {
getSendTokenAddress,
getAssetImages,
} from '../../../../selectors';
import { updateSendToken } from '../../../../ducks/send/send.duck';
import { updateTokenType } from '../../../../store/actions';
import {
updateSendErrors,
updateSendToken,
} from '../../../../ducks/send/send.duck';
import SendAssetRow from './send-asset-row.component';
function mapStateToProps(state) {
@ -24,6 +28,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
setSendToken: (token) => dispatch(updateSendToken(token)),
updateTokenType: (tokenAddress) => dispatch(updateTokenType(tokenAddress)),
updateSendErrors: (error) => {
dispatch(updateSendErrors(error));
},
};
}

View File

@ -6,6 +6,7 @@ import {
ETH_GAS_PRICE_FETCH_WARNING_KEY,
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
GAS_PRICE_EXCESSIVE_ERROR_KEY,
UNSENDABLE_ASSET_ERROR_KEY,
} from '../../../helpers/constants/error-keys';
import SendAmountRow from './send-amount-row';
import SendGasRow from './send-gas-row';
@ -17,6 +18,10 @@ export default class SendContent extends Component {
t: PropTypes.func,
};
state = {
unsendableAssetError: false,
};
static propTypes = {
updateGas: PropTypes.func,
showAddToAddressBookModal: PropTypes.func,
@ -32,6 +37,9 @@ export default class SendContent extends Component {
updateGas = (updateData) => this.props.updateGas(updateData);
setUnsendableAssetError = (unsendableAssetError) =>
this.setState({ unsendableAssetError });
render() {
const {
warning,
@ -41,6 +49,7 @@ export default class SendContent extends Component {
noGasPrice,
} = this.props;
const { unsendableAssetError } = this.state;
let gasError;
if (gasIsExcessive) gasError = GAS_PRICE_EXCESSIVE_ERROR_KEY;
else if (noGasPrice) gasError = GAS_PRICE_FETCH_FAILURE_ERROR_KEY;
@ -50,10 +59,13 @@ export default class SendContent extends Component {
<div className="send-v2__form">
{gasError && this.renderError(gasError)}
{isEthGasPrice && this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)}
{error && this.renderError()}
{unsendableAssetError && this.renderError(UNSENDABLE_ASSET_ERROR_KEY)}
{error && this.renderError(error)}
{warning && this.renderWarning()}
{this.maybeRenderAddContact()}
<SendAssetRow />
<SendAssetRow
setUnsendableAssetError={this.setUnsendableAssetError}
/>
<SendAmountRow updateGas={this.updateGas} />
<SendGasRow />
{this.props.showHexData && (
@ -97,12 +109,11 @@ export default class SendContent extends Component {
);
}
renderError(gasError = '') {
renderError(error) {
const { t } = this.context;
const { error } = this.props;
return (
<Dialog type="error" className="send__error-dialog">
{gasError === '' ? t(error) : t(gasError)}
{t(error)}
</Dialog>
);
}

View File

@ -218,8 +218,12 @@ export const transactionFeeSelector = function (state, txData) {
const conversionRate = conversionRateSelector(state);
const nativeCurrency = getNativeCurrency(state);
const {
txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {},
const { txParams: { value = '0x0', gas: gasLimit = '0x0' } = {} } = txData;
// if the gas price from our infura endpoint is null or undefined
// use the metaswap average price estimation as a fallback
let {
txParams: { gasPrice },
} = txData;
const fiatTransactionAmount = getValueFromWeiHex({

View File

@ -1234,6 +1234,21 @@ export function addToken(
};
}
export function updateTokenType(tokenAddress) {
return async (dispatch) => {
let token = {};
dispatch(showLoadingIndication());
try {
token = await promisifiedBackground.updateTokenType(tokenAddress);
} catch (error) {
log.error(error);
} finally {
dispatch(hideLoadingIndication());
}
return token;
};
}
export function removeToken(address) {
return (dispatch) => {
dispatch(showLoadingIndication());