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:
parent
bd38b02d8e
commit
85f17831a2
@ -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"
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user